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

Proposed by David Fernandez Gonzalez
Status: Merged
Merge reported by: David Fernandez Gonzalez
Merged at revision: 3acb478f6a74effd62974c3e94399922a0036306
Proposed branch: ~litios/ubuntu-cve-tracker:oval-refactor
Merge into: ubuntu-cve-tracker:master
Diff against target: 2793 lines (+1117/-1275)
3 files modified
scripts/generate-oval (+85/-96)
scripts/oval_lib.py (+1031/-1178)
test/gold_oval_structure/oval.xml (+1/-1)
Reviewer Review Type Date Requested Status
Eduardo Barretto Approve
Review via email: mp+455118@code.launchpad.net

Description of the change

This PR refactors the OVAL generation. The main changes are:

* Use of XML library.
* Creation of a common Generator class that shares most of the code needed to generate the OVAL elements.
* Refactor of CVE Generator and PKG generator to inherit from this class.
* Refactor of generate-oval to share a common interface
* New feature for pkg cache when the pkg version is not present: now it will use the lowest in the release if the binary versions differ.

The biggest impact will be on OVAL CVE, as the IDs, spacing, etc will differ now due to using the same one as PKG OVAL has been using.

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

Hey, I just did a quick check and found some small things to improve and wanted to mention already.
I will continue to check the code later and run some tests locally too.
Thanks for all this hard work :)

review: Needs Fixing
7365bc8... by David Fernandez Gonzalez

oval: increase generator speed

Signed-off-by: David Fernandez Gonzalez <email address hidden>

0574f1a... by David Fernandez Gonzalez

generate-oval: improvements

Signed-off-by: David Fernandez Gonzalez <email address hidden>

107e5ce... by David Fernandez Gonzalez

oval_lib: use CVE-based IDs for definitions for CVE OVAL

Signed-off-by: David Fernandez Gonzalez <email address hidden>

bec0e63... by David Fernandez Gonzalez

oval_lib: fix some bugs

Signed-off-by: David Fernandez Gonzalez <email address hidden>

29ca294... by David Fernandez Gonzalez

oval_lib: replace macos tag by linux

Signed-off-by: David Fernandez Gonzalez <email address hidden>

7239e32... by David Fernandez Gonzalez

oval_lib: bump OVAL generator version to 2

Signed-off-by: David Fernandez Gonzalez <email address hidden>

a42624b... by David Fernandez Gonzalez

oval_lib: use latest version binaries when vulnerable

e0a6581... by David Fernandez Gonzalez

oval_lib: PKG - fix ids and ignore packages without binaries

Signed-off-by: David Fernandez Gonzalez <email address hidden>

7595fed... by David Fernandez Gonzalez

OVAL: fix test to fit new linux tag

Signed-off-by: David Fernandez Gonzalez <email address hidden>

9bf1b0f... by David Fernandez Gonzalez

OVAL: CVE - use same IDs as old format

Signed-off-by: David Fernandez Gonzalez <email address hidden>

42bf4f9... by David Fernandez Gonzalez

OVAL: CVE/PKG no longer use tmp directories

Signed-off-by: David Fernandez Gonzalez <email address hidden>

08b5354... by David Fernandez Gonzalez

OVAL: prefix argument is not longer used for CVE/PKG

Signed-off-by: David Fernandez Gonzalez <email address hidden>

05c7540... by David Fernandez Gonzalez

OVAL: enable ocioutdir for PKG/CVE

Signed-off-by: David Fernandez Gonzalez <email address hidden>

Revision history for this message
Eduardo Barretto (ebarretto) wrote :

lgtm!
thanks for landing the last fixes!

review: Approve
89047d3... by David Fernandez Gonzalez

OVAL: improve USN generation code

 * Fix bug when not listing releases
 * Refactor generate_oval_usn code in generate-oval

Signed-off-by: David Fernandez Gonzalez <email address hidden>

3acb478... by David Fernandez Gonzalez

OVAL: make IDs smaller

Signed-off-by: David Fernandez Gonzalez <email address hidden>

Revision history for this message
David Fernandez Gonzalez (litios) wrote :

Thanks a lot for all the reviews and the patience to check everything!!

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 723127f..bdb9902 100755
3--- a/scripts/generate-oval
4+++ b/scripts/generate-oval
5@@ -96,8 +96,8 @@ def main():
6 '(default is ./)')
7 parser.add_argument('--usn-number', default=None, type=str,
8 help='if passed specifics a USN for the oval_usn generator')
9- parser.add_argument('--oval-release', default=None, type=str,
10- help='specifies a release to generate the oval usn')
11+ parser.add_argument('--oval-releases', default=None, action='append',
12+ help='specifies releases to generate the oval')
13 parser.add_argument('--packages', nargs='+', action='store', default=None,
14 help='generates oval for specific packages. Only for '
15 'CVE OVAL')
16@@ -132,27 +132,23 @@ def main():
17
18 if args.usn_oval:
19 if args.oci:
20- generate_oval_usn(args.output_dir, args.usn_number, args.oval_release,
21- args.cve_prefix_dir, args.usn_db_dir, ociprefix, ocioutdir)
22+ generate_oval_usn(args.output_dir, args.usn_number, args.oval_releases,
23+ args.cve_prefix_dir, args.usn_db_dir, args.no_progress, ociprefix, ocioutdir)
24 else:
25- generate_oval_usn(args.output_dir, args.usn_number, args.oval_release,
26- args.cve_prefix_dir, args.usn_db_dir)
27+ generate_oval_usn(args.output_dir, args.usn_number, args.oval_releases,
28+ args.cve_prefix_dir, args.usn_db_dir, args.no_progress,)
29
30 return
31
32 # if --oval-release, we still need to load parents cache
33 # so we can generate a complete oval for those releases that
34 # have a parent
35- if args.oval_release:
36- releases = [args.oval_release]
37- if args.oval_release not in supported_releases:
38- error(f"unknown oval release {args.oval_release}")
39- else:
40- r = args.oval_release
41- while release_parent(r):
42- r = release_parent(r)
43- releases.append(r)
44- supported_releases = releases
45+ releases = supported_releases
46+ if args.oval_releases:
47+ releases = args.oval_releases
48+ for release in releases:
49+ if release not in supported_releases:
50+ error(f"unknown oval release {release}")
51
52 cache = {}
53 for release in supported_releases:
54@@ -161,17 +157,15 @@ def main():
55
56 if args.pkg_oval:
57 if args.oci:
58- generate_oval_package(outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames, args.fixed_only, ociprefix, ocioutdir)
59+ generate_oval_package(releases, outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames, args.fixed_only, ocioutdir)
60 else:
61- generate_oval_package(outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames, args.fixed_only)
62+ generate_oval_package(releases, outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames, args.fixed_only)
63 return
64
65 if args.oci:
66- generate_oval_cve(outdir, args.cve_prefix_dir, cache, args.oci,
67- args.no_progress, args.packages, pathnames, ociprefix, ocioutdir)
68+ generate_oval_cve(releases, outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames, args.fixed_only, ocioutdir)
69 else:
70- generate_oval_cve(outdir, args.cve_prefix_dir, cache, args.oci,
71- args.no_progress, args.packages, pathnames)
72+ generate_oval_cve(releases, outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames, args.fixed_only)
73 return
74
75
76@@ -390,7 +384,7 @@ def get_usn_database(usn_db_dir):
77 # WARNING:
78 # be sure the release you are passing is in the usn-number passed
79 # otherwise it will generate an oval file without the usn info.
80-def generate_oval_usn(outdir, usn, usn_release, cve_dir, usn_db_dir, ociprefix=None, ocioutdir=None):
81+def generate_oval_usn(outdir, usn, usn_releases, cve_dir, usn_db_dir, no_progress, ociprefix=None, ocioutdir=None):
82 # Get the usn database.json data
83 usn_database = get_usn_database(usn_db_dir)
84 if not usn_database:
85@@ -400,33 +394,28 @@ def generate_oval_usn(outdir, usn, usn_release, cve_dir, usn_db_dir, ociprefix=N
86 if usn not in usn_database:
87 error("Please enter a valid USN number or update your database.json and try again")
88
89- if usn_release:
90- if usn_release not in supported_releases:
91- error("Please enter a valid release name.")
92-
93 # Create OvalGeneratorUSN objects
94 ovals = []
95- # Does the oval for just a specific given release
96- if usn_release:
97- ovals.append(oval_lib.OvalGeneratorUSN(usn_release, release_name(usn_release), outdir, cve_dir))
98- # Also produce oval generator object for OCI
99- if ocioutdir:
100- ovals.append(oval_lib.OvalGeneratorUSN(usn_release, release_name(usn_release), ocioutdir,
101- cve_dir, ociprefix, 'oci'))
102+ valid_releases = []
103+
104+ # Check or generate valid releases
105+ if usn_releases:
106+ for usn_release in usn_releases:
107+ if usn_release not in supported_releases:
108+ error(f"Invalid release name '{usn_release}'.")
109+ valid_releases = usn_releases
110 else:
111- for release in supported_releases:
112- # for now we don't differentiate products (e.g. esm) in the USN DB
113- product, series = product_series(release)
114- if product != PRODUCT_UBUNTU:
115- continue
116+ valid_releases = list(filter(lambda release: product_series(release)[0] == PRODUCT_UBUNTU, supported_releases))
117
118- ovals.append(oval_lib.OvalGeneratorUSN(release, release_name(release), outdir, cve_dir))
119- # Also produce oval generator object for OCI
120- if ocioutdir:
121- ovals.append(oval_lib.OvalGeneratorUSN(release, release_name(release), ocioutdir,
122- cve_dir, ociprefix,
123- 'oci'))
124+ if not no_progress:
125+ print('[*] Generating OVAL USN for packages in releases', ', '.join(valid_releases))
126
127+ for release in valid_releases:
128+ ovals.append(oval_lib.OvalGeneratorUSN(release, release_name(release), outdir, cve_dir))
129+ # Also produce oval generator object for OCI
130+ if ocioutdir:
131+ ovals.append(oval_lib.OvalGeneratorUSN(release, release_name(release), ocioutdir,
132+ cve_dir, ociprefix, 'oci'))
133 # Generate OVAL USN data
134 if usn:
135 prepend_usn_to_id(usn_database, usn)
136@@ -441,62 +430,62 @@ def generate_oval_usn(outdir, usn, usn_release, cve_dir, usn_db_dir, ociprefix=N
137 for oval in ovals:
138 oval.write_oval_elements()
139
140+ if not no_progress:
141+ print(f'[*] Done generating OVAL USN for packages in releases {", ".join(valid_releases)}')
142+
143 return True
144
145-def generate_oval_package(outdir, cve_prefix_dir, pkg_cache, cve_cache, oci, no_progress, packages, pathnames, fixed_only, ociprefix='', ocioutdir=None):
146- for release in supported_releases:
147- if not no_progress:
148- print(f'[*] Generating OVAL for packages in release {release}')
149- ov = oval_lib.OvalGeneratorPkg(release, release_name(release), pathnames, packages, not no_progress, pkg_cache=pkg_cache, fixed_only=fixed_only, cve_cache=cve_cache, oval_format='oci' if oci else 'dpkg', outdir=outdir, cve_prefix_dir=cve_prefix_dir, prefix=ociprefix)
150+def generate_oval_package(releases, outdir, cve_prefix_dir, pkg_cache, cve_cache, oci, no_progress, packages, pathnames, fixed_only, ocioutdir=None):
151+ if not no_progress:
152+ print(f'[*] Generating OVAL PKG for packages in releases {", ".join(releases)}')
153+
154+ ov = oval_lib.OvalGeneratorPkg(
155+ releases,
156+ pathnames,
157+ packages,
158+ not no_progress,
159+ pkg_cache=pkg_cache,
160+ fixed_only=fixed_only,
161+ cve_cache=cve_cache,
162+ oval_format='dpkg',
163+ outdir=outdir,
164+ cve_prefix_dir=cve_prefix_dir
165+ )
166+ ov.generate_oval()
167+
168+ if oci:
169+ ov.oval_format = 'oci'
170+ ov.output_dir = ocioutdir
171 ov.generate_oval()
172
173- if oci:
174- ov.oval_format = 'dpkg'
175- ov.generate_oval()
176-
177- if not no_progress:
178- print(f'[X] Done generating OVAL for packages in release {release}')
179-
180-def generate_oval_cve(outdir, cve_prefix_dir, cache, oci, no_progress, packages, pathnames, ociprefix=None, ocioutdir=None):
181- ovals = dict()
182- for release in supported_releases:
183- # we can have nested parent releases
184- parent = release_progenitor(release)
185- index = '{0}_dpkg'.format(release)
186- ovals[index] = oval_lib.OvalGeneratorCVE(release, release_name(release), parent, warn, outdir, prefix='', oval_format='dpkg')
187- ovals[index].add_release_applicability_definition()
188- if oci:
189- index = '{0}_oci'.format(release)
190- ovals[index] = oval_lib.OvalGeneratorCVE(release, release_name(release), parent, warn, ocioutdir, prefix=ociprefix, oval_format='oci')
191- ovals[index].add_release_applicability_definition()
192-
193- # loop through all CVE data files
194- files = []
195- for pathname in pathnames:
196- files = files + glob.glob(os.path.join(cve_prefix_dir, pathname))
197- files.sort()
198-
199- pkg_filter = None
200- if packages:
201- pkg_filter = packages
202-
203- files_count = len(files)
204- for i_file, filepath in enumerate(files):
205- cve_data = parse_cve_file(filepath, cache, pkg_filter)
206- # skip CVEs without packages for supported releases
207- if not cve_data['packages']:
208- if not no_progress:
209- progress_bar(i_file + 1, files_count)
210- continue
211-
212- for i in ovals:
213- ovals[i].generate_cve_definition(cve_data)
214-
215- if not no_progress:
216- progress_bar(i_file + 1, files_count)
217+ if not no_progress:
218+ print(f'[X] Done generating OVAL PKG for packages in releases {", ".join(releases)}')
219+
220+def generate_oval_cve(releases, outdir, cve_prefix_dir, pkg_cache, cve_cache, oci, no_progress, packages, pathnames, fixed_only, ocioutdir=None):
221+ if not no_progress:
222+ print(f'[*] Generating OVAL CVE for packages in releases {",".join(releases)}')
223+
224+ ov = oval_lib.OvalGeneratorCVE(
225+ releases,
226+ pathnames,
227+ packages,
228+ not no_progress,
229+ pkg_cache=pkg_cache,
230+ fixed_only=fixed_only,
231+ cve_cache=cve_cache,
232+ oval_format='dpkg',
233+ outdir=outdir,
234+ cve_prefix_dir=cve_prefix_dir
235+ )
236+ ov.generate_oval()
237+
238+ if oci:
239+ ov.oval_format = 'oci'
240+ ov.output_dir = ocioutdir
241+ ov.generate_oval()
242
243- for i in ovals:
244- ovals[i].write_to_file()
245+ if not no_progress:
246+ print(f'[X] Done generating OVAL CVE for packages in releases {", ".join(releases)}')
247
248 if __name__ == '__main__':
249 main()
250diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py
251index 655f0af..abe63dc 100644
252--- a/scripts/oval_lib.py
253+++ b/scripts/oval_lib.py
254@@ -22,7 +22,6 @@ from datetime import datetime, timezone
255 import apt_pkg
256 import io
257 import os
258-import random
259 import re
260 import shutil
261 import sys
262@@ -30,6 +29,7 @@ import tempfile
263 import collections
264 import glob
265 import xml.etree.cElementTree as etree
266+import json
267 from xml.dom import minidom
268 from typing import Tuple # Needed because of Python < 3.9 and to also support < 3.7
269
270@@ -41,6 +41,7 @@ from xml.sax.saxutils import escape
271 sources = {}
272 source_map_binaries = {}
273 debug_level = 0
274+GENERIC_VERSION = '0:0'
275
276 def recursive_rm(dirPath):
277 '''recursively remove directory'''
278@@ -143,80 +144,361 @@ def generate_cve_tag(cve):
279 cve_ref += '>{0}</cve>'.format(cve['Candidate'])
280 return cve_ref
281
282-def get_latest_version(versions):
283- latest = None
284- for version in versions:
285- if not latest:
286- latest = version
287- continue
288- elif apt_pkg.version_compare(version, latest) > 0:
289- latest = version
290-
291- return latest
292-
293-def get_binarypkgs(cache, source_name, release, version=None):
294+def get_binarypkgs(cache, source_name, release):
295 """ return a list of binary packages from the source package version """
296 packages_to_ignore = ("-dev", "-doc", "-dbg", "-dbgsym", "-udeb", "-locale-")
297- version_map = collections.defaultdict(list)
298- cache_version = version
299+ binaries_map = collections.defaultdict(dict)
300
301 if source_name not in cache[release]:
302 rel = release
303 while cve_lib.release_parent(rel):
304 rel = cve_lib.release_parent(rel)
305- r , sv, vm = get_binarypkgs(cache, source_name, rel, version)
306+ r , vb = get_binarypkgs(cache, source_name, rel)
307 if r:
308- return r, sv, vm
309+ return r, vb
310
311 # if a source package does not exist in such a release
312 # return None
313- return release, None, None
314- elif version and version not in cache[release][source_name]:
315- rel = release
316- while cve_lib.release_parent(rel):
317- rel = cve_lib.release_parent(rel)
318- r , sv, vm = get_binarypkgs(cache, source_name, rel, version)
319- if r:
320- return r, sv, vm
321-
322- # if version is not in release, then fetch latest source version
323- cache_version = get_latest_version(list(cache[release][source_name].keys()))
324- elif not version:
325- # if no version is provided, get latest source version
326- version = get_latest_version(list(cache[release][source_name].keys()))
327- cache_version = version
328- else:
329- cache_version = version
330+ return None, None
331+
332+ for source_version in cache[release][source_name]:
333+ binaries_map.setdefault(source_version, dict())
334+ for binary, bin_data in cache[release][source_name][source_version]['binaries'].items():
335+ # for kernel we only want linux images
336+ if source_name.startswith('linux') and not binary.startswith('linux-image-'):
337+ continue
338+ # skip ignored packages, with exception of golang*-dev pkgs
339+ if binary.startswith(('golang-go')) or \
340+ not any(s in binary for s in packages_to_ignore):
341+ binaries_map[source_version].setdefault(bin_data['version'], list())
342+ binaries_map[source_version][bin_data['version']].append(binary)
343+
344+ return release, binaries_map
345+
346+class CVEPkgRelEntry:
347+ def __init__(self, pkg, release, cve, status, note) -> None:
348+ self.pkg = pkg
349+ self.cve = cve
350+ self.orig_status = status
351+ self.orig_note = note
352+ self.release = release
353+ cve_info = CVEPkgRelEntry.parse_package_status(self.release, pkg.name, status, note, cve.number, None)
354+
355+ self.note = cve_info['note']
356+ self.status = cve_info['status']
357+ self.fixed_version = cve_info['fix-version'] if self.status == 'fixed' else None
358+
359+ @staticmethod
360+ def parse_package_status(release, package, status_text, note, filepath, cache):
361+ """ parse ubuntu package status string format:
362+ <status code> (<version/notes>)
363+ outputs dictionary: {
364+ 'status' : '<not-applicable | unknown | vulnerable | fixed>',
365+ 'note' : '<description of the status>',
366+ 'fix-version' : '<version with issue fixed, if applicable>',
367+ 'bin-pkgs' : []
368+ } """
369+
370+ # TODO fix for CVE Generator
371+
372+ # break out status code and detail
373+ code = status_text.lower()
374+ detail = note.strip('()') if note else None
375+ status = {}
376+ fix_version = ""
377+
378+ if detail and detail[0].isdigit() and len(detail.split(' ')) == 1:
379+ fix_version = detail
380+
381+ note_end = " (note: '{0}').".format(detail) if detail else '.'
382+ if code == 'dne':
383+ status['status'] = 'not-applicable'
384+ status['note'] = \
385+ " package does not exist in {0}{1}".format(release, note_end)
386+ elif code == 'ignored':
387+ status['status'] = 'vulnerable'
388+ status['note'] = ": while related to the CVE in some way, a decision has been made to ignore this issue{0}".format(note_end)
389+ elif code == 'not-affected':
390+ # check if there is a release version and if so, test for
391+ # package existence with that version
392+ if fix_version:
393+ status['status'] = 'fixed'
394+ status['note'] = " package in {0}, is related to the CVE in some way and has been fixed{1}".format(release, note_end)
395+ status['fix-version'] = fix_version
396+ else:
397+ status['status'] = 'not-vulnerable'
398+ status['note'] = " package in {0}, while related to the CVE in some way, is not affected{1}".format(release, note_end)
399+ elif code == 'needed':
400+ status['status'] = 'vulnerable'
401+ status['note'] = \
402+ " package in {0} is affected and needs fixing{1}".format(release, note_end)
403+ elif code == 'pending':
404+ # pending means that packages have been prepared and are in
405+ # -proposed or in a ppa somewhere, and should have a version
406+ # attached. If there is a version, test for package existence
407+ # with that version, otherwise mark as vulnerable
408+ if fix_version:
409+ status['status'] = 'fixed'
410+ status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(release, note_end)
411+ status['fix-version'] = fix_version
412+ else:
413+ status['status'] = 'vulnerable'
414+ status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(release, note_end)
415+ elif code == 'deferred':
416+ status['status'] = 'vulnerable'
417+ status['note'] = " package in {0} is affected, but a decision has been made to defer addressing it{1}".format(release, note_end)
418+ elif code in ['released']:
419+ # if there isn't a release version, then just mark
420+ # as vulnerable to test for package existence
421+ if not fix_version:
422+ status['status'] = 'vulnerable'
423+ status['note'] = " package in {0} was vulnerable and has been fixed, but no release version available for it{1}".format(release, note_end)
424+ else:
425+ status['status'] = 'fixed'
426+ status['note'] = " package in {0} was vulnerable but has been fixed{1}".format(release, note_end)
427+ status['fix-version'] = fix_version
428+ elif code == 'needs-triage':
429+ status['status'] = 'vulnerable'
430+ status['note'] = " package in {0} is affected and may need fixing{1}".format(release, note_end)
431+ else:
432+ # TODO LOGGIN
433+ print('Unsupported status "{0}" in {1}_{2} in "{3}". Setting to "unknown".'.format(code, release, package, filepath))
434+ status['status'] = 'unknown'
435+ status['note'] = " package in {0} has a vulnerability that is not known (status: '{1}'). It is pending evaluation{2}".format(release, code, note_end)
436+
437+ return status
438+
439+ def is_not_applicable(self) -> bool:
440+ return self.status in ['not-vulnerable', 'not-applicable']
441+
442+ def __str__(self) -> str:
443+ return f'{str(self.pkg)}:{self.status} {self.fixed_version}'
444+
445+class CVE:
446+ def __init__(self, number, info, pkgs=None) -> None:
447+ self.number = number
448+ self.description = info['Description']
449+ self.priority = info['Priority'][0]
450+ self.public_date = info['PublicDate']
451+ self.public_date_at_usn = info['PublicDateAtUSN'] if 'PublicDateAtUSN' in info else ''
452+ self.cvss = info['CVSS']
453+ self.assigned_to = info['Assigned-to'] if 'Assigned-to' in info else ''
454+ self.discoverd_by = info['Discovered-by'] if 'Discovered-by' in info else ''
455+ self.usns = []
456+ self.references = []
457+ self.bugs = []
458+ for url in info['References'].split('\n'):
459+ if 'https://ubuntu.com/security/notices/USN-' in url:
460+ self.usns.append(url[40:])
461+ elif re.match("https?:\/\/(bugs\.)?launchpad\.net\/(.*\/\+bug|bugs)\/\d+", url):
462+ self.bugs.append(url)
463+ elif url:
464+ self.references.append(url)
465+
466+ for bug in info['Bugs'].split('\n'):
467+ if bug:
468+ self.bugs.append(bug)
469+
470+ self.pkg_rel_entries = {}
471+ self.pkgs = pkgs if pkgs else []
472+
473+ def get_pkgs(self, releases):
474+ # We assume priority is as the order in the list
475+ pkgs = []
476+ pkg_rel = {}
477+ for pkg in self.pkgs:
478+ if pkg.rel not in releases:
479+ continue
480+
481+ if pkg.name not in pkg_rel:
482+ pkg_rel[pkg.name] = pkg.rel
483+ elif releases.index(pkg.rel) < releases.index(pkg_rel[pkg.name]):
484+ pkg_rel[pkg.name] = pkg.rel
485+
486+ for pkg in self.pkgs:
487+ if self.pkg_rel_entries[str(pkg)].is_not_applicable():
488+ continue
489+
490+ if pkg.name in pkg_rel and pkg_rel[pkg.name] == pkg.rel:
491+ pkgs.append(pkg)
492+
493+ return pkgs
494+
495+
496+ def add_pkg(self, pkg_object, release, state, note):
497+ cve_pkg_entry = CVEPkgRelEntry(pkg_object, release, self, state, note)
498+ self.pkg_rel_entries[str(pkg_object)] = cve_pkg_entry
499+ self.pkgs.append(pkg_object)
500+ pkg_object.add_cve(self)
501+
502+ def __str__(self) -> str:
503+ return self.number
504+
505+ def __repr__(self):
506+ return self.__str__()
507+
508+class Package:
509+ def __init__(self, pkgname, rel, versions_binaries):
510+ self.name = pkgname
511+ self.rel = rel
512+ self.description = cve_lib.lookup_package_override_description(pkgname)
513+
514+ if not self.description:
515+ if 'description' in sources[rel][pkgname]:
516+ self.description = sources[rel][pkgname]['description']
517+ elif pkgname in source_map_binaries[rel] and \
518+ 'description' in source_map_binaries[rel][pkgname]:
519+ self.description = source_map_binaries[rel][pkgname]['description']
520+ else:
521+ # Get first description found
522+ if 'binaries' in sources[self.rel][self.name]:
523+ for binary in sources[self.rel][self.name]['binaries']:
524+ if binary in source_map_binaries[self.rel] and 'description' in source_map_binaries[self.rel][binary]:
525+ self.description = source_map_binaries[self.rel][binary]["description"]
526+ break
527+
528+ self.section = sources[rel][pkgname]['section']
529+ self.versions_binaries = versions_binaries if versions_binaries else {}
530+ self.earliest_version = self.get_earliest_version()
531+ self.latest_version = self.get_latest_version()
532+
533+ binary_versions = self.get_binary_versions(self.earliest_version)
534+ self.is_kernel_pkg = False if len(binary_versions) == 0 else \
535+ is_kernel_binaries(self.get_binaries(self.earliest_version, binary_versions[0]))
536+ self.cves = []
537+
538+ def add_cve(self, cve) -> None:
539+ self.cves.append(cve)
540+
541+ def get_latest_version(self):
542+ latest = None
543+ for version in self.versions_binaries.keys():
544+ if not latest:
545+ latest = version
546+ continue
547+ elif apt_pkg.version_compare(version, latest) > 0:
548+ latest = version
549+
550+ return latest
551+
552+ def get_earliest_version(self):
553+ earliest = None
554+ for version in self.versions_binaries.keys():
555+ if not earliest:
556+ earliest = version
557+ continue
558+ elif apt_pkg.version_compare(earliest, version) > 0:
559+ earliest = version
560+
561+ return earliest
562+
563+ def version_exists(self, source_version):
564+ return source_version in self.versions_binaries
565+
566+ def all_binaries_same_version(self, source_version):
567+ if source_version not in self.versions_binaries:
568+ return len(self.versions_binaries[self.earliest_version]) <= 1
569+ return len(self.versions_binaries[source_version]) <= 1
570+
571+ def get_version_to_check(self, source_version):
572+ if not source_version:
573+ return self.latest_version
574+ else:
575+ if source_version in self.versions_binaries or self.all_binaries_same_version(source_version):
576+ return source_version
577+ else:
578+ if source_version and apt_pkg.version_compare(source_version, self.earliest_version) > 0:
579+ print(f'Wrong CVE entry version {source_version} - earliest for package {self.name} in {self.rel} is {self.earliest_version}')
580+
581+ return self.earliest_version
582+
583+ def get_binary_versions(self, source_version):
584+ if not self.versions_binaries: return []
585+
586+ if source_version not in self.versions_binaries:
587+ # If this is the case, package binaries should all have the same version
588+ # Relying on that, we can use the version of the CVE as the right version
589+ return [source_version]
590+ return list(self.versions_binaries[source_version].keys())
591+
592+ def get_binaries(self, source_version, binary_version):
593+ if not self.versions_binaries: return {}
594+ if source_version not in self.versions_binaries:
595+ if len(self.versions_binaries[self.earliest_version]) != 1:
596+ print(f"WARN: Version {source_version} doesn't exist yet the package {self.name} has different versions for the binaries")
597+
598+ version_binaries = self.versions_binaries[self.earliest_version]
599+ return version_binaries[self.get_binary_versions(self.earliest_version)[0]]
600+ return self.versions_binaries[source_version][binary_version]
601+
602+ def __str__(self) -> str:
603+ return f"{self.name}/{self.rel}"
604
605- for binary, bin_data in cache[release][source_name][cache_version]['binaries'].items():
606- # for kernel we only want linux images
607- if source_name.startswith('linux') and not binary.startswith('linux-image-'):
608- continue
609- # skip ignored packages, with exception of golang*-dev pkgs
610- if binary.startswith(('golang-go')) or \
611- not any(s in binary for s in packages_to_ignore):
612- version_map[bin_data['version']].append(binary)
613+ def __repr__(self):
614+ return self.__str__()
615+
616+class USN:
617+ def __init__(self, data):
618+ for item in ['description', 'releases', 'title', 'timestamp', 'summary', 'action', 'cves', 'id', 'isummary']:
619+ if item in data:
620+ setattr(self, item, data[item])
621+ else:
622+ setattr(self, item, None)
623+
624+ def __str__(self) -> str:
625+ return self.id
626
627- return release, version, version_map
628+ def __repr__(self) -> str:
629+ return self.id
630
631+# Oval Generators
632 class OvalGenerator:
633 supported_oval_elements = ('definition', 'test', 'object', 'state', 'variable')
634- generator_version = '1.1'
635+ generator_version = '2'
636 oval_schema_version = '5.11.1'
637- def __init__(self, release, release_name = None, warn_method=False, outdir='./', prefix='', oval_format='dpkg') -> None:
638- self.release = release
639- # e.g. codename for trusty/esm should be trusty
640- self.release_codename = cve_lib.release_progenitor(release) if cve_lib.release_progenitor(release) else self.release.replace('/', '_')
641- self.release_name = release_name
642- #self.warn = warn_method or self.warn
643- self.tmpdir = tempfile.mkdtemp(prefix='oval_lib-')
644+ def __init__(self, type, releases, cve_paths, packages, progress, pkg_cache, fixed_only=True, cve_cache=None, cve_prefix_dir=None, outdir='./', oval_format='dpkg') -> None:
645+ self.releases = releases
646 self.output_dir = outdir
647 self.oval_format = oval_format
648+ self.generator_type = type
649+ self.progress = progress
650+ self.cve_cache = cve_cache
651+ self.pkg_cache = pkg_cache
652+ self.cve_paths = cve_paths
653+ self.fixed_only = fixed_only
654+ self.packages, self.cves = self._load(cve_prefix_dir, packages)
655+
656+ def _init_ids(self, release):
657+ # e.g. codename for trusty/esm should be trusty
658+ self.release = release
659+ self.release_codename = cve_lib.release_progenitor(self.release) if cve_lib.release_progenitor(self.release) else self.release.replace('/', '_')
660+ self.release_name = cve_lib.release_name(self.release)
661+
662+ self.parent_releases = set()
663+ current_release = self.release
664+ while(cve_lib.release_parent(current_release)):
665+ current_release = cve_lib.release_parent(current_release)
666+ if current_release != self.release:
667+ self.parent_releases.add(current_release)
668+
669 self.ns = 'oval:com.ubuntu.{0}'.format(self.release_codename)
670 self.id = 100
671 self.host_def_id = self.id
672 self.release_applicability_definition_id = '{0}:def:{1}0'.format(self.ns, self.id)
673-
674+ ###
675+ # ID schema: 2204|00001|0001
676+ # * The first four digits are the ubuntu release number
677+ # * The next 5 digits is # just a package counter, we increase it for each definition
678+ # * The last 4 digits is a counter for the criterion
679+ ###
680+ release_code = int(self.release_name.split(' ')[1].replace('.', '')) if self.release not in cve_lib.external_releases else 1111
681+ self.release_id = release_code * 10 ** 10
682+ self.definition_id = self.release_id
683+ self.definition_step = 1 * 10 ** 5
684+ self.criterion_step = 10
685+ self.output_filepath = \
686+ '{0}com.ubuntu.{1}.{2}.oval.xml'.format('oci.' if self.oval_format == 'oci' else '', self.release.replace('/', '_'), self.generator_type)
687+
688 def _add_structure(self, root) -> None:
689 structure = {}
690 for element in self.supported_oval_elements:
691@@ -225,21 +507,11 @@ class OvalGenerator:
692
693 return structure
694
695- def _get_root_element(self, type) -> etree.Element:
696+ def _get_generator(self, type) -> etree.Element:
697 oval_timestamp = datetime.now(tz=timezone.utc).strftime(
698 '%Y-%m-%dT%H:%M:%S')
699
700- root_element = etree.Element("oval_definitions", attrib= {
701- "xmlns":"http://oval.mitre.org/XMLSchema/oval-definitions-5",
702- "xmlns:ind-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#independent",
703- "xmlns:oval":"http://oval.mitre.org/XMLSchema/oval-common-5",
704- "xmlns:unix-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#unix",
705- "xmlns:linux-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#linux",
706- "xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance" ,
707- "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"
708- })
709-
710- generator = etree.SubElement(root_element, "generator")
711+ generator = etree.Element("generator")
712 product_name = etree.SubElement(generator, "oval:product_name")
713 product_version = etree.SubElement(generator, "oval:product_version")
714 schema_version = etree.SubElement(generator, "oval:schema_version")
715@@ -250,6 +522,19 @@ class OvalGenerator:
716 schema_version.text = self.oval_schema_version
717 timestamp.text = oval_timestamp
718
719+ return generator
720+
721+ def _get_root_element(self) -> etree.Element:
722+ root_element = etree.Element("oval_definitions", attrib= {
723+ "xmlns":"http://oval.mitre.org/XMLSchema/oval-definitions-5",
724+ "xmlns:ind-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#independent",
725+ "xmlns:oval":"http://oval.mitre.org/XMLSchema/oval-common-5",
726+ "xmlns:unix-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#unix",
727+ "xmlns:linux-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#linux",
728+ "xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance" ,
729+ "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#linux linux-definitions-schema.xsd"
730+ })
731+
732 xml_tree = etree.ElementTree(root_element)
733 return xml_tree, root_element
734
735@@ -396,229 +681,90 @@ class OvalGenerator:
736
737 return family_state, state
738
739-class CVEPkgRelEntry:
740- def __init__(self, pkg, release, cve, status, note, cache) -> None:
741- self.pkg = pkg
742- self.cve = cve
743- self.orig_status = status
744- self.orig_note = note
745- self.release = release
746- cve_info = CVEPkgRelEntry.parse_package_status(self.release, pkg.name, status, note, cve.number, cache)
747+ def _add_new_package(self, package_name, cve, release, cve_data, packages) -> None:
748+ if package_name not in packages:
749+ _, versions_binaries = get_binarypkgs(self.pkg_cache, package_name, release)
750+ pkg_obj = Package(package_name, release, versions_binaries)
751+ packages[package_name] = pkg_obj
752
753- self.note = cve_info['note']
754- self.status = cve_info['status']
755- self.fixed_version = cve_info['fix-version'] if self.status == 'fixed' else None
756+ pkg_obj = packages[package_name]
757+ cve.add_pkg(pkg_obj, release, cve_data['pkgs'][package_name][release][0],cve_data['pkgs'][package_name][release][1])
758
759- @staticmethod
760- def parse_package_status(release, package, status_text, note, filepath, cache):
761- """ parse ubuntu package status string format:
762- <status code> (<version/notes>)
763- outputs dictionary: {
764- 'status' : '<not-applicable | unknown | vulnerable | fixed>',
765- 'note' : '<description of the status>',
766- 'fix-version' : '<version with issue fixed, if applicable>',
767- 'bin-pkgs' : []
768- } """
769+ def _load(self, cve_prefix_dir, packages_filter=None) -> None:
770+ cve_lib.load_external_subprojects()
771
772- # TODO fix for CVE Generator
773+ cve_paths = []
774+ for pathname in self.cve_paths:
775+ cve_paths = cve_paths + glob.glob(os.path.join(cve_prefix_dir, pathname))
776
777- # break out status code and detail
778- code = status_text.lower()
779- detail = note.strip('()') if note else None
780- status = {}
781- fix_version = None
782+ cve_paths.sort(key=lambda cve:
783+ (int(cve.split('/')[-1].split('-')[1]), int(cve.split('/')[-1].split('-')[2])) \
784+ if cve.split('/')[-1].split('-')[2].isnumeric() \
785+ else (int(cve.split('/')[-1].split('-')[1]), 0)
786+ )
787
788- if detail and detail[0].isdigit() and len(detail.split(' ')) == 1:
789- fix_version = detail
790+ packages = {}
791+ cves = {}
792+ base_releases = self.releases
793+ final_releases = set(self.releases)
794+ for current_release in base_releases:
795+ while(cve_lib.release_parent(current_release)):
796+ current_release = cve_lib.release_parent(current_release)
797+ final_releases.add(current_release)
798+
799+ for release in final_releases:
800+ packages.setdefault(release, {})
801+ cves.setdefault(release, {})
802+ sources[release] = load(releases=[release], skip_eol_releases=False)[release]
803
804- parent = release
805- if cache and code != 'dne':
806- parent, status['source-version'], status['bin-pkgs'] = get_binarypkgs(cache, package, release, version=fix_version)
807- if parent != release:
808- status['parent'] = parent
809+ orig_name = cve_lib.get_orig_rel_name(release)
810+ if '/' in orig_name:
811+ orig_name = orig_name.split('/', maxsplit=1)[1]
812+ source_map_binaries[release] = load(data_type='packages',releases=[orig_name], skip_eol_releases=False)[orig_name] \
813+ if release not in cve_lib.external_releases else {}
814
815- note_end = " (note: '{0}').".format(detail) if detail else '.'
816- if code == 'dne':
817- status['status'] = 'not-applicable'
818- status['note'] = \
819- " package does not exist in {0}{1}".format(release, note_end)
820- elif code == 'ignored':
821- status['status'] = 'vulnerable'
822- status['note'] = ": while related to the CVE in some way, a decision has been made to ignore this issue{0}".format(note_end)
823- elif code == 'not-affected':
824- # check if there is a release version and if so, test for
825- # package existence with that version
826- if fix_version:
827- status['status'] = 'fixed'
828- status['note'] = " package in {0}, is related to the CVE in some way and has been fixed{1}".format(parent, note_end)
829- status['fix-version'] = fix_version
830- else:
831- status['status'] = 'not-vulnerable'
832- status['note'] = " package in {0}, while related to the CVE in some way, is not affected{1}".format(parent, note_end)
833- elif code == 'needed':
834- status['status'] = 'vulnerable'
835- status['note'] = \
836- " package in {0} is affected and needs fixing{1}".format(parent, note_end)
837- elif code == 'pending':
838- # pending means that packages have been prepared and are in
839- # -proposed or in a ppa somewhere, and should have a version
840- # attached. If there is a version, test for package existence
841- # with that version, otherwise mark as vulnerable
842- if fix_version:
843- status['status'] = 'fixed'
844- status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(parent, note_end)
845- status['fix-version'] = fix_version
846- else:
847- status['status'] = 'vulnerable'
848- status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(parent, note_end)
849- elif code == 'deferred':
850- status['status'] = 'vulnerable'
851- status['note'] = " package in {0} is affected, but a decision has been made to defer addressing it{1}".format(parent, note_end)
852- elif code in ['released']:
853- # if there isn't a release version, then just mark
854- # as vulnerable to test for package existence
855- if not fix_version:
856- status['status'] = 'vulnerable'
857- status['note'] = " package in {0} was vulnerable and has been fixed, but no release version available for it{1}".format(parent, note_end)
858- else:
859- status['status'] = 'fixed'
860- status['note'] = " package in {0} was vulnerable but has been fixed{1}".format(parent, note_end)
861- status['fix-version'] = fix_version
862- elif code == 'needs-triage':
863- status['status'] = 'vulnerable'
864- status['note'] = " package in {0} is affected and may need fixing{1}".format(parent, note_end)
865- else:
866- # TODO LOGGIN
867- print('Unsupported status "{0}" in {1}_{2} in "{3}". Setting to "unknown".'.format(code, release, package, filepath))
868- status['status'] = 'unknown'
869- status['note'] = " package in {0} has a vulnerability that is not known (status: '{1}'). It is pending evaluation{2}".format(release, code, note_end)
870-
871- return status
872-
873- def __str__(self) -> str:
874- return f'{str(self.pkg)}:{self.status} {self.fixed_version}'
875-
876-class CVE:
877- def __init__(self, number, info, pkgs=[]) -> None:
878- self.number = number
879- self.description = info['Description']
880- self.priority = info['Priority'][0]
881- self.public_date = info['PublicDate']
882- self.cvss = info['CVSS']
883- self.usns = []
884- for url in info['References'].split('\n'):
885- if 'https://ubuntu.com/security/notices/USN-' in url:
886- self.usns.append(url[40:])
887- self.pkg_rel_entries = {}
888- self.pkgs = pkgs
889-
890- def add_pkg(self, pkg_object, cve_pkg_entry):
891- if cve_pkg_entry.status in ['not-vulnerable', 'not-applicable']:
892- return
893-
894- self.pkg_rel_entries[pkg_object.name] = cve_pkg_entry
895- self.pkgs.append(pkg_object)
896- pkg_object.cves.append(self)
897-
898- def __str__(self) -> str:
899- return self.number
900-
901- def __repr__(self):
902- return self.__str__()
903-
904-class Package:
905- def __init__(self, pkgname, rel, binaries, version):
906- self.name = pkgname
907- self.rel = rel
908- self.description = cve_lib.lookup_package_override_description(pkgname)
909-
910- if not self.description:
911- if 'description' in sources[rel][pkgname]:
912- self.description = sources[rel][pkgname]['description']
913- elif pkgname in source_map_binaries[rel] and \
914- 'description' in source_map_binaries[rel][pkgname]:
915- self.description = source_map_binaries[rel][pkgname]['description']
916- else:
917- # Get first description found
918- if 'binaries' in sources[self.rel][self.name]:
919- for binary in sources[self.rel][self.name]['binaries']:
920- if binary in source_map_binaries[self.rel] and 'description' in source_map_binaries[self.rel][binary]:
921- self.description = source_map_binaries[self.rel][binary]["description"]
922- break
923-
924- self.section = sources[rel][pkgname]['section']
925- self.version = version
926- self.binaries = binaries if binaries else []
927- self.cves = []
928-
929- def add_cve(self, cve) -> None:
930- self.cves.append(cve)
931-
932- def __str__(self) -> str:
933- return f"{self.name}/{self.rel}"
934-
935- def __repr__(self):
936- return self.__str__()
937-
938-class OvalGeneratorPkg(OvalGenerator):
939- def __init__(self, release, release_name, cve_paths, packages, progress, pkg_cache, fixed_only=True, cve_cache=None, cve_prefix_dir=None, warn_method=False, outdir='./', prefix='', oval_format='dpkg') -> None:
940- super().__init__(release, release_name, warn_method, outdir, prefix, oval_format)
941- self.progress = progress
942- self.cve_cache = cve_cache
943- self.pkg_cache = pkg_cache
944- self.cve_paths = cve_paths
945- self.fixed_only = fixed_only
946- self.packages = self._load_pkgs(cve_prefix_dir, packages)
947-
948- def _reset(self):
949- ###
950- # ID schema: 2204|00001|0001
951- # * The first four digits are the ubuntu release number
952- # * The next 5 digits is # just a package counter, we increase it for each definition
953- # * The last 4 digits is a counter for the criterion
954- ###
955- release_code = int(self.release_name.split(' ')[1].replace('.', '')) if self.release not in cve_lib.external_releases else 1111
956- self.definition_id = release_code * 10 ** 10
957- self.definition_step = 1 * 10 ** 5
958- self.criterion_step = 10
959- self.output_filepath = \
960- '{0}com.ubuntu.{1}.pkg.oval.xml'.format('oci.' if self.oval_format == 'oci' else '', self.release.replace('/', '_'))
961+ i = 0
962+ for cve_path in cve_paths:
963+ cve_number = cve_path.rsplit('/', 1)[1]
964+ i += 1
965
966- def _generate_advisory(self, package: Package) -> etree.Element:
967- advisory = etree.Element("advisory")
968- rights = etree.SubElement(advisory, "rights")
969- component = etree.SubElement(advisory, "component")
970- version = etree.SubElement(advisory, "current_version")
971+ if self.progress:
972+ print(f'[{i:5}/{len(cve_paths)}] Processing {cve_number:18}', end='\r')
973
974- for cve in package.cves:
975- if self.fixed_only and cve.pkg_rel_entries[package.name].status != 'fixed':
976- continue
977- cve_obj = self._generate_cve_tag(cve)
978- advisory.append(cve_obj)
979+ if not cve_number in self.cve_cache:
980+ self.cve_cache[cve_number] = cve_lib.load_cve(cve_path)
981
982- rights.text = f"Copyright (C) {datetime.now().year} Canonical Ltd."
983- component.text = package.section
984- version.text = package.version
985+ info = self.cve_cache[cve_number]
986+ cve_obj = CVE(cve_number, info)
987+ for pkg in info['pkgs']:
988+ if packages_filter and pkg not in packages_filter:
989+ continue
990
991- return advisory
992+ for release in final_releases:
993+ if pkg in sources[release] and release in info['pkgs'][pkg] and \
994+ info['pkgs'][pkg][release][0] != 'DNE':
995+ self._add_new_package(pkg, cve_obj, release, info, packages[release])
996+ if cve_number not in cves[release]:
997+ cves[release][cve_number] = cve_obj
998
999- def _generate_metadata(self, package: Package) -> etree.Element:
1000- metadata = etree.Element("metadata")
1001- title = etree.SubElement(metadata, "title")
1002- reference = self._generate_reference(package)
1003- advisory = self._generate_advisory(package)
1004- metadata.append(reference)
1005- description = etree.SubElement(metadata, "description")
1006- affected = etree.SubElement(metadata, "affected", attrib = {"family": "unix"})
1007- platform = etree.SubElement(affected, "platform")
1008- metadata.append(advisory)
1009+ for release in final_releases:
1010+ packages[release] = dict(sorted(packages[release].items()))
1011+ cves[release] = dict(sorted(cves[release].items()))
1012
1013- platform.text = self.release_name
1014- title.text = package.name
1015- description.text = package.description
1016+ if self.progress:
1017+ print(' ' * 40, end='\r')
1018+ return packages, cves
1019
1020- return metadata
1021+ def _write_oval_xml(self, xml_tree: etree.ElementTree, root_element: etree.ElementTree) -> None:
1022+ if sys.version_info[0] >= 3 and sys.version_info[1] >= 9:
1023+ etree.indent(xml_tree, level=0) # indent only available from Python 3.9
1024+ xml_tree.write(os.path.join(self.output_dir, self.output_filepath))
1025+ else:
1026+ xmlstr = minidom.parseString(etree.tostring(root_element)).toprettyxml(indent=" ")
1027+ with open(os.path.join(self.output_dir, self.output_filepath), 'w') as file:
1028+ file.write(xmlstr)
1029
1030+ # Object generators
1031 def _generate_criteria(self) -> etree.Element:
1032 criteria = etree.Element("criteria")
1033 if self.oval_format == 'dpkg':
1034@@ -629,38 +775,15 @@ class OvalGeneratorPkg(OvalGenerator):
1035 extend_definition.set("applicability_check", "true")
1036
1037 return criteria
1038-
1039- def _generate_subcriteria(self, operator) -> etree.Element:
1040- return etree.Element("criteria", attrib={
1041- "operator": operator
1042- })
1043-
1044- def _generate_criterion_element(self, comment, id) -> etree.Element:
1045- criterion = etree.Element("criterion", attrib={
1046- "test_ref": f"{self.ns}:tst:{id}",
1047- "comment": comment
1048- })
1049-
1050- return criterion
1051-
1052- # Element generators
1053- def _generate_reference(self, package) -> etree.Element:
1054- reference = etree.Element("reference", attrib={
1055- "source": "Package",
1056- "ref_id": package.name,
1057- "ref_url": f'https://launchpad.net/ubuntu/+source/{package.name}'
1058- })
1059-
1060- return reference
1061-
1062- def _generate_definition_element(self, package) -> None:
1063+
1064+ def _generate_definition_object(self, object) -> etree.Element:
1065 id = f"{self.ns}:def:{self.definition_id}"
1066 definition = etree.Element("definition")
1067 definition.set("class", "vulnerability")
1068 definition.set("id", id)
1069 definition.set("version", "1")
1070
1071- metadata = self._generate_metadata(package)
1072+ metadata = self._generate_metadata(object)
1073 criteria = self._generate_criteria()
1074 definition.append(metadata)
1075 definition.append(criteria)
1076@@ -692,7 +815,7 @@ class OvalGeneratorPkg(OvalGenerator):
1077 cve_tag.set('usns', ','.join(cve.usns))
1078
1079 return cve_tag
1080-
1081+
1082 def _generate_var_element(self, comment, id, binaries) -> etree.Element:
1083 var = etree.Element("constant_variable",
1084 attrib={
1085@@ -813,6 +936,72 @@ class OvalGeneratorPkg(OvalGenerator):
1086
1087 return object
1088
1089+ def _generate_criterion_element(self, comment, id) -> etree.Element:
1090+ criterion = etree.Element("criterion", attrib={
1091+ "test_ref": f"{self.ns}:tst:{id}",
1092+ "comment": comment
1093+ })
1094+
1095+ return criterion
1096+
1097+ def _generate_vulnerable_elements(self, package, binaries, obj_id=None):
1098+ binary_keyword = 'binaries' if len(binaries) > 1 else 'binary'
1099+ test_note = f"Does the '{package.name}' package exist?"
1100+ object_note = f"The '{package.name}' package {binary_keyword}"
1101+
1102+ test = self._generate_test_element(test_note, self.definition_id, False, 'pkg', obj_id=obj_id)
1103+
1104+ if not obj_id:
1105+ object = self._generate_object_element(object_note, self.definition_id, self.definition_id)
1106+
1107+ if package.is_kernel_pkg:
1108+ regex = process_kernel_binaries(binaries, 'oci')
1109+ binaries = [f'{regex}']
1110+
1111+ final_binaries = []
1112+ if self.oval_format == 'oci':
1113+ variable_values = '(?::\w+|)\s+(.*)$'
1114+ for binary in binaries:
1115+ final_binaries.append(f'^{binary}{variable_values}')
1116+ else:
1117+ final_binaries = binaries
1118+
1119+ var = self._generate_var_element(object_note, self.definition_id, final_binaries)
1120+ else:
1121+ object = None
1122+ var = None
1123+ return test, object, var
1124+
1125+ def _generate_fixed_elements(self, package, binaries, version, obj_id=None):
1126+ binary_keyword = 'binaries' if len(binaries) > 1 else 'binary'
1127+ test_note = f"Does the '{package.name}' package exist and is the version less than '{version}'?"
1128+ object_note = f"The '{package.name}' package {binary_keyword}"
1129+ state_note = f"The package version is less than '{version}'"
1130+
1131+ test = self._generate_test_element(test_note, self.definition_id, True, 'pkg', obj_id=obj_id)
1132+ if not obj_id:
1133+ object = self._generate_object_element(object_note, self.definition_id, self.definition_id)
1134+
1135+ final_binaries = binaries
1136+ if self.oval_format == 'oci':
1137+ if package.is_kernel_pkg:
1138+ regex = process_kernel_binaries(binaries, 'oci')
1139+ final_binaries = [f'^{regex}(?::\w+|)\s+(.*)$']
1140+ else:
1141+ variable_values = '(?::\w+|)\s+(.*)$'
1142+
1143+ final_binaries = []
1144+ for binary in binaries:
1145+ final_binaries.append(f'^{binary}{variable_values}')
1146+
1147+ var = self._generate_var_element(object_note, self.definition_id, final_binaries)
1148+ else:
1149+ object = None
1150+ var = None
1151+ state = self._generate_state_element(state_note, self.definition_id, version)
1152+
1153+ return test, object, var, state
1154+
1155 # Running kernel element generators
1156 def _add_running_kernel_checks(self, root_element):
1157 objects = root_element.find("objects")
1158@@ -890,6 +1079,11 @@ class OvalGeneratorPkg(OvalGenerator):
1159 return test
1160
1161 # Kernel elements generators
1162+ def _generate_criteria_kernel(self, operator) -> etree.Element:
1163+ return etree.Element("criteria", attrib={
1164+ "operator": operator
1165+ })
1166+
1167 def _generate_kernel_version_object_element(self, id, var_id) -> etree.Element:
1168 object = etree.Element("ind-def:variable_object",
1169 attrib={
1170@@ -924,12 +1118,12 @@ class OvalGeneratorPkg(OvalGenerator):
1171 value.text = f"0:{patched}"
1172 return state
1173
1174- def _generate_kernel_package_elements(self, package: Package, root_element, running_kernel_check_id) -> etree.Element:
1175+ def _generate_kernel_package_elements(self, package: Package, binaries, root_element, running_kernel_check_id) -> etree.Element:
1176 tests = root_element.find("tests")
1177 states = root_element.find("states")
1178
1179 comment_running_kernel = f'Is kernel {package.name} running?'
1180- regex = process_kernel_binaries(package.binaries, self.oval_format)
1181+ regex = process_kernel_binaries(binaries, self.oval_format)
1182
1183 criterion_running_kernel = self._generate_criterion_element(comment_running_kernel, self.definition_id)
1184 test_running_kernel = self._generate_test_element_running_kernel(self.definition_id, comment_running_kernel, running_kernel_check_id)
1185@@ -942,22 +1136,25 @@ class OvalGeneratorPkg(OvalGenerator):
1186
1187 return criterion_running_kernel
1188
1189- def _add_fixed_kernel_elements(self, cve: CVE, package: Package, package_rel_entry:CVEPkgRelEntry, root_element, running_kernel_id, fixed_versions) -> etree.Element:
1190+ def _add_kernel_elements(self, cve: CVE, package: Package, version, package_rel_entry:CVEPkgRelEntry, root_element, running_kernel_id, fixed_versions) -> etree.Element:
1191 tests = root_element.find("tests")
1192 objects = root_element.find("objects")
1193 states = root_element.find("states")
1194
1195 comment_version = f'Kernel {package.name} version comparison'
1196- comment_criterion = f'({cve.number}) {package.name} {package_rel_entry.note}'
1197+ comment_criterion = ''
1198+ if self.generator_type == 'pkg':
1199+ comment_criterion = f'({cve.number}) '
1200+ comment_criterion = comment_criterion + f'{package.name}{package_rel_entry.note}'
1201
1202- if package_rel_entry.fixed_version in fixed_versions:
1203- criterion_version = self._generate_criterion_element(comment_criterion, fixed_versions[package_rel_entry.fixed_version])
1204+ if version in fixed_versions:
1205+ criterion_version = self._generate_criterion_element(comment_criterion, fixed_versions[version])
1206 else:
1207 create_state = False
1208
1209- if package_rel_entry.fixed_version:
1210+ if version:
1211 create_state = True
1212- ste_kernel_version = self._generate_state_kernel_element("Kernel check", self.definition_id, package_rel_entry.fixed_version)
1213+ ste_kernel_version = self._generate_state_kernel_element("Kernel check", self.definition_id, version)
1214 states.append(ste_kernel_version)
1215
1216 obj_kernel_version = self._generate_kernel_version_object_element(self.definition_id, running_kernel_id)
1217@@ -969,7 +1166,7 @@ class OvalGeneratorPkg(OvalGenerator):
1218 tests.append(test_kernel_version)
1219 objects.append(obj_kernel_version)
1220
1221- fixed_versions[package_rel_entry.fixed_version] = self.definition_id
1222+ fixed_versions[version] = self.definition_id
1223
1224 return criterion_version
1225
1226@@ -977,8 +1174,10 @@ class OvalGeneratorPkg(OvalGenerator):
1227 def _increase_id(self, is_definition):
1228 if is_definition:
1229 self.definition_id += self.definition_step
1230- clean_value = self.definition_step / 10
1231- self.definition_id = int(int(self.definition_id / clean_value) * clean_value)
1232+ # Ugly hack, Python doesn't like operating big numbers
1233+ criterion_appendix_length = len(str(self.definition_step)) - 1
1234+ self.definition_id = int(str(self.definition_id)[: -1 * criterion_appendix_length])
1235+ self.definition_id = int(self.definition_id * self.definition_step)
1236 else:
1237 self.definition_id += self.criterion_step
1238
1239@@ -994,99 +1193,177 @@ class OvalGeneratorPkg(OvalGenerator):
1240 criteria.append(element)
1241
1242 def _add_criterion(self, id, package_entry, cve, definition, depth=2) -> None:
1243- criterion_note = f'({cve.number}) {package_entry.pkg.name}{package_entry.note}'
1244+ criterion_note = f'({cve.number}) ' if self.generator_type == 'pkg' else ''
1245+ criterion_note += f'{package_entry.pkg.name}{package_entry.note}'
1246 criterion = self._generate_criterion_element(criterion_note, id)
1247 self._add_to_criteria(definition, criterion, depth)
1248
1249- def _generate_elements(self, package, binaries, pkg_rel_entry, obj_id=None):
1250+ def _generate_elements(self, package, binaries, version, pkg_rel_entry, obj_id=None):
1251 create_state = False
1252 state = None
1253 var = None
1254 obj = None
1255- binary_keyword = 'binaries' if len(package.binaries) > 1 else 'binary'
1256+ binary_keyword = 'binaries' if len(binaries) > 1 else 'binary'
1257 object_note = f"The '{package.name}' package {binary_keyword}"
1258 test_note = ""
1259
1260+ final_binaries = binaries
1261 if self.oval_format == 'oci':
1262- if is_kernel_binaries(package.binaries):
1263- regex = process_kernel_binaries(package.binaries, 'oci')
1264- binaries = [f'^{regex}(?::\w+|)\s+(.*)$']
1265+ if package.is_kernel_pkg:
1266+ regex = process_kernel_binaries(binaries, 'oci')
1267+ final_binaries = [f'^{regex}(?::\w+|)\s+(.*)$']
1268 else:
1269 variable_values = '(?::\w+|)\s+(.*)$'
1270
1271- binaries = []
1272- for binary in package.binaries:
1273- binaries.append(f'^{binary}{variable_values}')
1274+ final_binaries = []
1275+ for binary in binaries:
1276+ final_binaries.append(f'^{binary}{variable_values}')
1277
1278 if pkg_rel_entry.status == 'vulnerable':
1279 test_note = f"Does the '{package.name}' package exist?"
1280 elif pkg_rel_entry.status == 'fixed':
1281- test_note = f"Does the '{package.name}' package exist and is the version less than '{pkg_rel_entry.fixed_version}'?"
1282- state_note = f"The package version is less than '{pkg_rel_entry.fixed_version}'"
1283+ test_note = f"Does the '{package.name}' package exist and is the version less than '{version}'?"
1284+ state_note = f"The package version is less than '{version}'"
1285
1286- state = self._generate_state_element(state_note, self.definition_id, pkg_rel_entry.fixed_version)
1287+ state = self._generate_state_element(state_note, self.definition_id, version)
1288 create_state = True
1289
1290- if not obj_id or create_state:
1291- obj_id = None
1292-
1293- var = self._generate_var_element(object_note, self.definition_id, binaries)
1294-
1295+ if not obj_id:
1296+ var = self._generate_var_element(object_note, self.definition_id, final_binaries)
1297 obj = self._generate_object_element(object_note, self.definition_id, self.definition_id)
1298
1299 test = self._generate_test_element(test_note, self.definition_id, create_state, 'pkg', obj_id=obj_id)
1300
1301 return test, obj, var, state
1302
1303- def _populate_pkg(self, package, root_element):
1304- tests = root_element.find("tests")
1305- objects = root_element.find("objects")
1306- variables = root_element.find("variables")
1307- states = root_element.find("states")
1308+ # returns True if we should ignore this source package; primarily used
1309+ # for -edge kernels
1310+ def _ignore_source_package(self, source):
1311+ if re.match('linux-.*-edge$', source):
1312+ return True
1313+ if re.match('linux-riscv.*$', source):
1314+ # linux-riscv.* currently causes a lot of false positives, skip
1315+ # it altogether while we don't land a better fix
1316+ return True
1317+ return False
1318
1319- # Add package definition
1320- definitions = root_element.find("definitions")
1321- definition_element = self._generate_definition_element(package)
1322
1323- # Control/cache variables
1324- one_time_added_id = None
1325- fixed_versions = {}
1326- binaries_id = None
1327- cve_added = False
1328+class OvalGeneratorPkg(OvalGenerator):
1329+ def __init__(self, releases, cve_paths, packages, progress, pkg_cache, fixed_only=True, cve_cache=None, cve_prefix_dir=None, outdir='./', oval_format='dpkg') -> None:
1330+ super().__init__('pkg', releases, cve_paths, packages, progress, pkg_cache, fixed_only, cve_cache, cve_prefix_dir, outdir, oval_format)
1331
1332- #criteria = None
1333- #if len(package.binaries) > 1:
1334- # criteria = self._generate_subcriteria('AND')
1335+ def _generate_advisory(self, package: Package) -> etree.Element:
1336+ advisory = etree.Element("advisory")
1337+ rights = etree.SubElement(advisory, "rights")
1338+ component = etree.SubElement(advisory, "component")
1339+ version = etree.SubElement(advisory, "current_version")
1340
1341 for cve in package.cves:
1342- pkg_rel_entry = cve.pkg_rel_entries[package.name]
1343- for key in sorted(list(package.binaries)):
1344- binaries = package.binaries[key]
1345- if pkg_rel_entry.fixed_version:
1346- if pkg_rel_entry.fixed_version in fixed_versions:
1347- self._add_test_ref_to_cve_tag(fixed_versions[pkg_rel_entry.fixed_version], cve, definition_element)
1348- self._add_criterion(fixed_versions[pkg_rel_entry.fixed_version], pkg_rel_entry, cve, definition_element)
1349- continue
1350- else:
1351- self._add_test_ref_to_cve_tag(self.definition_id, cve, definition_element)
1352- self._add_criterion(self.definition_id, pkg_rel_entry, cve, definition_element)
1353- fixed_versions[pkg_rel_entry.fixed_version] = self.definition_id
1354- elif one_time_added_id:
1355- self._add_test_ref_to_cve_tag(one_time_added_id, cve, definition_element)
1356- self._add_criterion(one_time_added_id, pkg_rel_entry, cve, definition_element)
1357- continue
1358- else:
1359- self._add_test_ref_to_cve_tag(self.definition_id, cve, definition_element)
1360- self._add_criterion(self.definition_id, pkg_rel_entry, cve, definition_element)
1361- one_time_added_id = self.definition_id
1362+ if self.fixed_only and cve.pkg_rel_entries[str(package)].status != 'fixed':
1363+ continue
1364+ elif cve.pkg_rel_entries[str(package)].is_not_applicable():
1365+ continue
1366+ cve_obj = self._generate_cve_tag(cve)
1367+ advisory.append(cve_obj)
1368
1369- test, obj, var, state = self._generate_elements(package, binaries, pkg_rel_entry, binaries_id)
1370+ rights.text = f"Copyright (C) {datetime.now().year} Canonical Ltd."
1371+ component.text = package.section
1372+ version.text = package.get_latest_version()
1373+
1374+ return advisory
1375+
1376+ def _generate_metadata(self, package: Package) -> etree.Element:
1377+ metadata = etree.Element("metadata")
1378+ title = etree.SubElement(metadata, "title")
1379+ reference = self._generate_reference(package)
1380+ advisory = self._generate_advisory(package)
1381+ metadata.append(reference)
1382+ description = etree.SubElement(metadata, "description")
1383+ affected = etree.SubElement(metadata, "affected", attrib = {"family": "unix"})
1384+ platform = etree.SubElement(affected, "platform")
1385+ metadata.append(advisory)
1386+
1387+ platform.text = self.release_name
1388+ title.text = package.name
1389+ description.text = package.description
1390+
1391+ return metadata
1392+
1393+ # Element generators
1394+ def _generate_reference(self, package) -> etree.Element:
1395+ reference = etree.Element("reference", attrib={
1396+ "source": "Package",
1397+ "ref_id": package.name,
1398+ "ref_url": f'https://launchpad.net/ubuntu/+source/{package.name}'
1399+ })
1400+
1401+ return reference
1402+
1403+ def _populate_pkg(self, package, root_element):
1404+ tests = root_element.find("tests")
1405+ objects = root_element.find("objects")
1406+ variables = root_element.find("variables")
1407+ states = root_element.find("states")
1408+
1409+ # Add package definition
1410+ definitions = root_element.find("definitions")
1411+ definition_element = self._generate_definition_object(package)
1412+
1413+ # Control/cache variables
1414+ one_time_added_id = None
1415+ fixed_versions = {}
1416+ binaries_ids = {}
1417+ cve_added = False
1418+
1419+ #criteria = None
1420+ #if len(package.binaries) > 1:
1421+ # criteria = self._generate_subcriteria('AND')
1422+
1423+ for cve in package.cves:
1424+ if self.fixed_only and cve.pkg_rel_entries[str(package)].status != 'fixed':
1425+ continue
1426+ pkg_rel_entry = cve.pkg_rel_entries[str(package)]
1427+ if pkg_rel_entry.is_not_applicable(): continue
1428+ source_version = package.get_version_to_check(pkg_rel_entry.fixed_version)
1429+ for binary_version in package.get_binary_versions(source_version):
1430+ binaries = package.get_binaries(source_version, binary_version)
1431+
1432+ # For released / not affected (version) CVEs
1433+ if pkg_rel_entry.fixed_version:
1434+ if binary_version in fixed_versions:
1435+ self._add_test_ref_to_cve_tag(fixed_versions[binary_version], cve, definition_element)
1436+ self._add_criterion(fixed_versions[binary_version], pkg_rel_entry, cve, definition_element)
1437+ continue
1438+ else:
1439+ self._add_test_ref_to_cve_tag(self.definition_id, cve, definition_element)
1440+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, definition_element)
1441+ fixed_versions[binary_version] = self.definition_id
1442+ # For not fixed CVEs, we already added one for this package
1443+ elif one_time_added_id:
1444+ self._add_test_ref_to_cve_tag(one_time_added_id, cve, definition_element)
1445+ self._add_criterion(one_time_added_id, pkg_rel_entry, cve, definition_element)
1446+ continue
1447+ # For not fixed CVEs, only need to add it once per package
1448+ else:
1449+ self._add_test_ref_to_cve_tag(self.definition_id, cve, definition_element)
1450+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, definition_element)
1451+ one_time_added_id = self.definition_id
1452+
1453+ # If version doesn't exist and only one binary_version, they all have the same binaries
1454+ if not package.version_exists(source_version) and package.all_binaries_same_version:
1455+ binary_id_version = GENERIC_VERSION
1456+ else:
1457+ binary_id_version = binary_version
1458+
1459+ binaries_ids.setdefault(binary_id_version, None)
1460+ binaries_id = binaries_ids[binary_id_version]
1461+ test, obj, var, state = self._generate_elements(package, binaries, binary_version, pkg_rel_entry, binaries_id)
1462
1463 if state:
1464 states.append(state)
1465
1466 if obj and var:
1467- binaries_id = self.definition_id
1468+ binaries_ids[binary_id_version] = self.definition_id
1469 variables.append(var)
1470 objects.append(obj)
1471
1472@@ -1099,29 +1376,39 @@ class OvalGeneratorPkg(OvalGenerator):
1473
1474 self._increase_id(is_definition=True)
1475
1476- def _populate_kernel_pkg(self, package, root_element, running_kernel_id):
1477+ def _populate_kernel_pkg(self, package, root_element, running_kernel_id):
1478+ for cve in package.cves:
1479+ pkg_rel_entry = cve.pkg_rel_entries[str(package)]
1480+ version_to_check = package.get_version_to_check(pkg_rel_entry.fixed_version)
1481+ for binary_version in package.get_binary_versions(version_to_check):
1482+ binaries = package.get_binaries(version_to_check, binary_version)
1483 # Add package definition
1484 definitions = root_element.find("definitions")
1485- definition_element = self._generate_definition_element(package)
1486+ definition_element = self._generate_definition_object(package)
1487
1488 # Control/cache variables
1489 fixed_versions = {}
1490 cve_added = False
1491
1492+ # Kernel binaries have all same version
1493+ version = package.get_latest_version()
1494+ binaries = package.get_binaries(version, version)
1495+
1496 # Generate one-time elements
1497- kernel_criterion = self._generate_kernel_package_elements(package, root_element, running_kernel_id)
1498- criteria = self._generate_subcriteria('OR')
1499+ kernel_criterion = self._generate_kernel_package_elements(package, binaries, root_element, running_kernel_id)
1500+ criteria = self._generate_criteria_kernel('OR')
1501
1502 self._add_to_criteria(definition_element, kernel_criterion, operator='AND')
1503 self._add_to_criteria(definition_element, criteria, operator='AND')
1504
1505 for cve in package.cves:
1506- pkg_rel_entry = cve.pkg_rel_entries[package.name]
1507+ pkg_rel_entry = cve.pkg_rel_entries[str(package)]
1508+ if pkg_rel_entry.is_not_applicable(): continue
1509 cve_added = True
1510
1511 self._add_test_ref_to_cve_tag(self.definition_id, cve, definition_element)
1512
1513- kernel_version_criterion = self._add_fixed_kernel_elements(cve, package, pkg_rel_entry, root_element, running_kernel_id, fixed_versions)
1514+ kernel_version_criterion = self._add_kernel_elements(cve, package, pkg_rel_entry.fixed_version, pkg_rel_entry, root_element, running_kernel_id, fixed_versions)
1515 self._add_to_criteria(definition_element, kernel_version_criterion, depth=3)
1516 self._increase_id(is_definition=False)
1517
1518@@ -1129,881 +1416,447 @@ class OvalGeneratorPkg(OvalGenerator):
1519 definitions.append(definition_element)
1520 self._increase_id(is_definition=True)
1521
1522- def _add_new_package(self, package_name, cve, release, cve_data, packages) -> None:
1523- if package_name not in packages:
1524- _, version, binaries = get_binarypkgs(self.pkg_cache, package_name, release)
1525-
1526- pkg_obj = Package(package_name, release, binaries, version)
1527- packages[package_name] = pkg_obj
1528-
1529- pkg_obj = packages[package_name]
1530- cve_pkg_entry = CVEPkgRelEntry(pkg_obj, release, cve, cve_data['pkgs'][package_name][release][0], cve_data['pkgs'][package_name][release][1], self.pkg_cache)
1531-
1532- if cve_pkg_entry.status != 'fixed' and self.fixed_only:
1533- return
1534-
1535- cve.add_pkg(pkg_obj, cve_pkg_entry)
1536-
1537- def _load_pkgs(self, cve_prefix_dir, packages_filter=None) -> None:
1538- cve_lib.load_external_subprojects()
1539-
1540- cves = []
1541- for pathname in self.cve_paths:
1542- cves = cves + glob.glob(os.path.join(cve_prefix_dir, pathname))
1543-
1544- cves.sort(key=lambda cve:
1545- (int(cve.split('/')[-1].split('-')[1]), int(cve.split('/')[-1].split('-')[2])) \
1546- if cve.split('/')[-1].split('-')[2].isnumeric() \
1547- else (int(cve.split('/')[-1].split('-')[1]), 0)
1548- )
1549-
1550- packages = {}
1551- releases = [self.release]
1552- current_release = self.release
1553- while(cve_lib.release_parent(current_release)):
1554- current_release = cve_lib.release_parent(current_release)
1555- releases.append(current_release)
1556-
1557- for release in releases:
1558- sources[release] = load(releases=[release], skip_eol_releases=False)[release]
1559-
1560- orig_name = cve_lib.get_orig_rel_name(release)
1561- if '/' in orig_name:
1562- orig_name = orig_name.split('/', maxsplit=1)[1]
1563- source_map_binaries[release] = load(data_type='packages',releases=[orig_name], skip_eol_releases=False)[orig_name] \
1564- if release not in cve_lib.external_releases else {}
1565-
1566- i = 0
1567- for cve_path in cves:
1568- cve_number = cve_path.rsplit('/', 1)[1]
1569- i += 1
1570-
1571- if self.progress:
1572- print(f'[{i:5}/{len(cves)}] Processing {cve_number:18}', end='\r')
1573-
1574- if not cve_number in self.cve_cache:
1575- self.cve_cache[cve_number] = cve_lib.load_cve(cve_path)
1576-
1577- info = self.cve_cache[cve_number]
1578- cve_obj = CVE(cve_number, info)
1579-
1580- for pkg in info['pkgs']:
1581- if packages_filter and pkg not in packages_filter:
1582- continue
1583-
1584- for release in releases:
1585- if pkg in sources[release] and release in info['pkgs'][pkg] and \
1586- info['pkgs'][pkg][release][0] != 'DNE':
1587- self._add_new_package(pkg, cve_obj, release, info, packages)
1588- break
1589-
1590- packages = dict(sorted(packages.items()))
1591- if self.progress:
1592- print(' ' * 40, end='\r')
1593- return packages
1594-
1595 def generate_oval(self) -> None:
1596- self._reset()
1597- xml_tree, root_element = self._get_root_element("Package")
1598- self._add_structure(root_element)
1599+ for release in self.releases:
1600+ self._init_ids(release)
1601+ xml_tree, root_element = self._get_root_element()
1602+ generator = self._get_generator("Package")
1603+ root_element.append(generator)
1604+ self._add_structure(root_element)
1605
1606- if self.oval_format == 'dpkg':
1607- # One time kernel check
1608- self._add_release_checks(root_element)
1609- self._add_running_kernel_checks(root_element)
1610- running_kernel_id = self.definition_id
1611- self._increase_id(is_definition=True)
1612+ if self.oval_format == 'dpkg':
1613+ # One time kernel check
1614+ self._add_release_checks(root_element)
1615+ self._add_running_kernel_checks(root_element)
1616+ running_kernel_id = self.definition_id
1617+ self._increase_id(is_definition=True)
1618+
1619+ all_pkgs = dict()
1620+ for parent_release in list(self.parent_releases)[::-1]:
1621+ all_pkgs.update(self.packages[parent_release])
1622+
1623+ all_pkgs.update(self.packages[self.release])
1624+
1625+ for pkg in all_pkgs:
1626+ if self._ignore_source_package(pkg): continue
1627+ if not all_pkgs[pkg].versions_binaries: continue
1628+ if not all_pkgs[pkg].get_binary_versions(next(iter(all_pkgs[pkg].versions_binaries))): continue
1629+ if all_pkgs[pkg].is_kernel_pkg and self.oval_format != 'oci':
1630+ self._populate_kernel_pkg(all_pkgs[pkg], root_element, running_kernel_id)
1631+ else:
1632+ self._populate_pkg(all_pkgs[pkg], root_element)
1633
1634- for pkg in self.packages:
1635- if len(self.packages[pkg].binaries) == 0:
1636- continue
1637+ self._write_oval_xml(xml_tree, root_element)
1638
1639- if is_kernel_binaries(self.packages[pkg].binaries) and self.oval_format != 'oci':
1640- self._populate_kernel_pkg(self.packages[pkg], root_element, running_kernel_id)
1641- else:
1642- self._populate_pkg(self.packages[pkg], root_element)
1643+class OvalGeneratorCVE(OvalGenerator):
1644+ def __init__(self, releases, cve_paths, packages, progress, pkg_cache, fixed_only=True, cve_cache=None, cve_prefix_dir=None, outdir='./', oval_format='dpkg') -> None:
1645+ super().__init__('cve', releases, cve_paths, packages, progress, pkg_cache, fixed_only, cve_cache, cve_prefix_dir, outdir, oval_format)
1646
1647- #etree.indent(xml_tree, level=0) -> only available from Python 3.9
1648- xmlstr = minidom.parseString(etree.tostring(root_element)).toprettyxml(indent=" ")
1649+ # For CVE OVAL, the definition ID is generated
1650+ # from the CVE ID
1651+ def _set_definition_id(self, cve_id):
1652+ self.definition_id = int(re.sub('[^0-9]', '', cve_id)) * self.definition_step
1653
1654- with open(os.path.join(self.output_dir, self.output_filepath), 'w') as file:
1655- file.write(xmlstr)
1656- #xml_tree.write(os.path.join(self.output_dir, self.output_filepath))
1657- return
1658+ def _generate_advisory(self, cve: CVE) -> etree.Element:
1659+ advisory = etree.Element("advisory")
1660+ severity = etree.SubElement(advisory, "severity")
1661+ rights = etree.SubElement(advisory, "rights")
1662+ public_date = etree.SubElement(advisory, "public_date")
1663
1664-class OvalGeneratorCVE:
1665- supported_oval_elements = ('definition', 'test', 'object', 'state',
1666- 'variable')
1667- generator_version = '1.1'
1668- oval_schema_version = '5.11.1'
1669+ if cve.public_date_at_usn:
1670+ public_date_at_usn = etree.SubElement(advisory, "public_date_at_usn")
1671+ public_date_at_usn.text = cve.public_date_at_usn
1672
1673- def __init__(self, release, release_name, parent, warn_method=False, outdir='./', prefix='', oval_format='dpkg'):
1674- """ constructor, set defaults for instances """
1675+ if cve.assigned_to:
1676+ assigned_to = etree.SubElement(advisory, "assigned_to")
1677+ assigned_to.text = cve.assigned_to
1678+
1679+ if cve.discoverd_by:
1680+ discoverd_by = etree.SubElement(advisory, "discoverd_by")
1681+ discoverd_by.text = cve.discoverd_by
1682
1683- self.release = release
1684- # e.g. codename for trusty/esm should be trusty
1685- self.release_codename = cve_lib.release_progenitor(release) if cve_lib.release_progenitor(release) else self.release.replace('/', '_')
1686- self.release_name = release_name
1687- self.warn = warn_method or self.warn
1688- self.tmpdir = tempfile.mkdtemp(prefix='oval_lib-')
1689- self.output_dir = outdir
1690- self.oval_format = oval_format
1691- self.output_filepath = \
1692- '{0}com.ubuntu.{1}.cve.oval.xml'.format(prefix, self.release.replace('/', '_'))
1693- self.ns = 'oval:com.ubuntu.{0}'.format(self.release_codename)
1694- self.id = 10
1695- self.release_applicability_definition_id = '{0}:def:{1}0'.format(self.ns, self.id)
1696+ for bug in cve.bugs:
1697+ element = etree.SubElement(advisory, 'bug')
1698+ element.text = bug
1699
1700- def __del__(self):
1701- """ deconstructor, clean up """
1702- if os.path.exists(self.tmpdir):
1703- recursive_rm(self.tmpdir)
1704-
1705- def generate_cve_definition(self, cve):
1706- """ generate an OVAL definition based on parsed CVE data """
1707-
1708- header = cve['header']
1709- # if the multiplier is not large enough, the tests IDs will
1710- # overlap on things with large numbers of binary packages.
1711- # if we ever have an issue that touches more than 1,000,000
1712- # binary packages, that will cause a problem.
1713- id_base = int(re.sub('[^0-9]', '', header['Candidate'])) * 1000000
1714- if not self.unique_id_base(id_base, header['Source-note']):
1715- self.warn('Calculated id_base "{0}" based on candidate value "{1}" is not unique. Skipping CVE.'.format(id_base, header['Candidate']))
1716-
1717- instruction = ""
1718- # make test(s) for each package
1719- test_refs = []
1720- packages = cve['packages']
1721- for package in sorted(packages.keys()):
1722- if self.release in packages[package]['Releases']:
1723- release_status = packages[package]['Releases'][self.release]
1724- if 'bin-pkgs' in release_status and release_status['bin-pkgs']:
1725- for key in sorted(list(release_status['bin-pkgs'])):
1726- pkg = {
1727- 'name': package,
1728- 'binaries': release_status['bin-pkgs'][key],
1729- 'status': release_status['status'],
1730- 'note': release_status['note'],
1731- 'fix-version': release_status['fix-version'] if 'fix-version' in release_status else '',
1732- 'id_base': id_base + len(test_refs),
1733- 'source-note': header['Source-note']
1734- }
1735- if is_kernel_binaries(pkg['binaries']):
1736- test_ref = self.get_running_kernel_testref(pkg)
1737- if test_ref:
1738- test_refs = test_refs + test_ref
1739- pkg['id_base'] = id_base + 1
1740- else:
1741- test_ref = self.get_oval_test_for_package(pkg)
1742- if test_ref:
1743- test_refs.append(test_ref)
1744- # prepare update instructions if package is fixed
1745- if pkg['status'] == 'fixed':
1746- if 'parent' in release_status:
1747- product_description = cve_lib.get_subproject_description(release_status['parent'])
1748- else:
1749- product_description = cve_lib.get_subproject_description(self.release)
1750- instruction = prepare_instructions(instruction, header['Candidate'], product_description, pkg)
1751-
1752-
1753-
1754- # if no packages for this release, then we're done
1755- if not len(test_refs):
1756- return False
1757-
1758- # convert CVE data to OVAL definition metadata
1759- mapping = {
1760- 'ns': escape(self.ns),
1761- 'id_base': id_base,
1762- 'codename': escape(self.release_codename),
1763- 'release_name': escape(self.release_name),
1764- 'applicability_def_id': escape(
1765- self.release_applicability_definition_id),
1766- 'cve_title': escape(header['Candidate']),
1767- 'description': escape('{0} {1}'.format(header['Description'],
1768- header['Ubuntu-Description']).strip() + instruction),
1769- 'priority': escape(header['Priority']),
1770- 'criteria': '',
1771- 'references': '',
1772- 'notes': ''
1773- }
1774+ for usn in cve.usns:
1775+ element = etree.SubElement(advisory, 'ref')
1776+ element.text = f'https://ubuntu.com/security/notices/USN-{usn}'
1777
1778- # convert test_refs to criteria
1779- if len(test_refs) == 1:
1780- negation_attribute = 'negate = "true" ' \
1781- if 'negate' in test_refs[0] and test_refs[0]['negate'] else ''
1782- mapping['criteria'] = \
1783- '<criterion test_ref="{0}" comment="{1}" {2}/>'.format(
1784- test_refs[0]['id'], escape(test_refs[0]['comment']), negation_attribute)
1785- else:
1786- criteria = []
1787- criteria.append('<criteria operator="OR">')
1788- for test_ref in test_refs:
1789- if 'kernel' in test_ref:
1790- criteria.append(' <criteria operator="AND">')
1791- negation_attribute = 'negate = "true" ' \
1792- if 'negate' in test_ref and test_ref['negate'] else ''
1793- criteria.append(
1794- ' <criterion test_ref="{0}" comment="{1}" {2}/>'.format(
1795- test_ref['id'],
1796- escape(test_ref['comment']), negation_attribute))
1797- elif 'kernelobj' in test_ref:
1798- criteria.append(
1799- ' <criterion test_ref="{0}" comment="{1}" {2}/>'.format(
1800- test_ref['id'],
1801- escape(test_ref['comment']), negation_attribute))
1802- criteria.append(' </criteria>')
1803- else:
1804- negation_attribute = 'negate = "true" ' \
1805- if 'negate' in test_ref and test_ref['negate'] else ''
1806- criteria.append(
1807- ' <criterion test_ref="{0}" comment="{1}" {2}/>'.format(
1808- test_ref['id'],
1809- escape(test_ref['comment']), negation_attribute))
1810- criteria.append('</criteria>')
1811- mapping['criteria'] = '\n '.join(criteria)
1812-
1813- # convert notes
1814- if header['Notes']:
1815- mapping['notes'] = '\n <oval:notes>' + \
1816- '\n <oval:note>{0}</oval:note>'.format(escape(header['Notes'])) + \
1817- '\n </oval:notes>'
1818-
1819- # convert additional data <advisory> metadata elements
1820- advisory = []
1821- advisory.append('<severity>{0}</severity>'.format(
1822- escape(header['Priority'].title())))
1823- advisory.append(
1824- '<rights>Copyright (C) {0}Canonical Ltd.</rights>'.format(escape(
1825- header['PublicDate'].split('-', 1)[0] + ' '
1826- if header['PublicDate'] else '')))
1827- if header['PublicDate']:
1828- advisory.append('<public_date>{0}</public_date>'.format(
1829- escape(header['PublicDate'])))
1830- if header['PublicDateAtUSN']:
1831- advisory.append(
1832- '<public_date_at_usn>{0}</public_date_at_usn>'.format(escape(
1833- header['PublicDateAtUSN'])))
1834- if header['Assigned-to']:
1835- advisory.append('<assigned_to>{0}</assigned_to>'.format(escape(
1836- header['Assigned-to'])))
1837- if header['Discovered-by']:
1838- advisory.append('<discovered_by>{0}</discovered_by>'.format(escape(
1839- header['Discovered-by'])))
1840- if header['CRD']:
1841- advisory.append('<crd>{0}</crd>'.format(escape(header['CRD'])))
1842- for bug in header['Bugs']:
1843- advisory.append('<bug>{0}</bug>'.format(escape(bug)))
1844- for ref in header['References']:
1845- if ref.startswith('https://cve.mitre'):
1846- cve_title = ref.split('=')[-1].strip()
1847- if not cve_title:
1848- continue
1849- mapping['cve_title'] = escape(cve_title)
1850- mapping['references'] = '\n <reference source="CVE" ref_id="{0}" ref_url="{1}" />'.format(mapping['cve_title'], escape(ref))
1851+ advisory.append(self._generate_cve_tag(cve))
1852+ rights.text = f"Copyright (C) {cve.public_date.split('-', 1)[0]} Canonical Ltd."
1853+ severity.text = cve.priority.capitalize()
1854+ public_date.text = cve.public_date
1855
1856- cve_ref = generate_cve_tag(header)
1857- advisory.append(cve_ref)
1858- mapping['advisory_elements'] = '\n '.join(advisory)
1859+ return advisory
1860
1861- if self.oval_format == 'dpkg':
1862- mapping['os_release_check'] = """<extend_definition definition_ref="{applicability_def_id}" comment="{release_name} ({codename}) is installed." applicability_check="true" />""".format(**mapping)
1863- else:
1864- mapping['os_release_check'] = ''
1865-
1866- self.queue_element('definition', """
1867- <definition class="vulnerability" id="{ns}:def:{id_base}0" version="1">
1868- <metadata>
1869- <title>{cve_title} on {release_name} ({codename}) - {priority}.</title>
1870- <description>{description}</description>
1871- <affected family="unix">
1872- <platform>{release_name}</platform>
1873- </affected>{references}
1874- <advisory>
1875- {advisory_elements}
1876- </advisory>
1877- </metadata>{notes}
1878- <criteria>
1879- {os_release_check}
1880- {criteria}
1881- </criteria>
1882- </definition>\n""".format(**mapping))
1883+ def _generate_metadata(self, cve: CVE) -> etree.Element:
1884+ metadata = etree.Element("metadata")
1885+ title = etree.SubElement(metadata, "title")
1886+ reference = self._generate_reference(cve)
1887+ advisory = self._generate_advisory(cve)
1888+ description = etree.SubElement(metadata, "description")
1889+ affected = etree.SubElement(metadata, "affected", attrib = {"family": "unix"})
1890+ platform = etree.SubElement(affected, "platform")
1891+ metadata.append(reference)
1892+ metadata.append(advisory)
1893
1894- # TODO: xml lib
1895- def add_release_applicability_definition(self):
1896- """ add platform/release applicability OVAL definition for codename """
1897+ platform.text = self.release_name
1898+ title.text = f'{cve.number} on {self.release_name} ({self.release_codename}) - {cve.priority}'
1899+ description.text = cve.description.replace('\n','')
1900
1901- mapping = {
1902- 'ns': self.ns,
1903- 'id_base': self.id,
1904- 'codename': self.release_codename,
1905- 'release_name': self.release_name,
1906- }
1907- self.release_applicability_definition_id = \
1908- '{ns}:def:{id_base}0'.format(**mapping)
1909+ return metadata
1910
1911- if self.oval_format == 'dpkg':
1912- self.queue_element('definition', """
1913- <definition class="inventory" id="{ns}:def:{id_base}0" version="1">
1914- <metadata>
1915- <title>Check that {release_name} ({codename}) is installed.</title>
1916- <description></description>
1917- </metadata>
1918- <criteria>
1919- <criterion test_ref="{ns}:tst:{id_base}0" comment="The host is part of the unix family." />
1920- <criterion test_ref="{ns}:tst:{id_base}1" comment="The host is running Ubuntu {codename}." />
1921- </criteria>
1922- </definition>\n""".format(**mapping))
1923-
1924- self.queue_element('test', """
1925- <ind-def:family_test id="{ns}:tst:{id_base}0" check="at least one" check_existence="at_least_one_exists" version="1" comment="Is the host part of the unix family?">
1926- <ind-def:object object_ref="{ns}:obj:{id_base}0"/>
1927- <ind-def:state state_ref="{ns}:ste:{id_base}0"/>
1928- </ind-def:family_test>
1929-
1930- <ind-def:textfilecontent54_test id="{ns}:tst:{id_base}1" check="at least one" check_existence="at_least_one_exists" version="1" comment="Is the host running Ubuntu {codename}?">
1931- <ind-def:object object_ref="{ns}:obj:{id_base}1"/>
1932- <ind-def:state state_ref="{ns}:ste:{id_base}1"/>
1933- </ind-def:textfilecontent54_test>\n""".format(**mapping))
1934-
1935- # /etc/lsb-release has to be a single path, due to some
1936- # environments (namely snaps) not being allowed to list the
1937- # content of /etc/
1938- self.queue_element('object', """
1939- <ind-def:family_object id="{ns}:obj:{id_base}0" version="1" comment="The singleton family object."/>
1940-
1941- <ind-def:textfilecontent54_object id="{ns}:obj:{id_base}1" version="1" comment="The singleton release codename object.">
1942- <ind-def:filepath>/etc/lsb-release</ind-def:filepath>
1943- <ind-def:pattern operation="pattern match">^[\\s\\S]*DISTRIB_CODENAME=([a-z]+)$</ind-def:pattern>
1944- <ind-def:instance datatype="int">1</ind-def:instance>
1945- </ind-def:textfilecontent54_object>\n""".format(**mapping))
1946-
1947- self.queue_element('state', """
1948- <ind-def:family_state id="{ns}:ste:{id_base}0" version="1" comment="The singleton family object.">
1949- <ind-def:family>unix</ind-def:family>
1950- </ind-def:family_state>
1951-
1952- <ind-def:textfilecontent54_state id="{ns}:ste:{id_base}1" version="1" comment="{release_name}">
1953- <ind-def:subexpression>{codename}</ind-def:subexpression>
1954- </ind-def:textfilecontent54_state>\n""".format(**mapping))
1955-
1956- def get_oval_test_for_package(self, package):
1957- """ create OVAL test and dependent objects for this package status
1958- @package = {
1959- 'name' : '<package name>',
1960- 'binaries' : [ '<binary_pkg_name', '<binary_pkg_name', ... ],
1961- 'status' : '<not-applicable | unknown | vulnerable | fixed>',
1962- 'note' : '<a description of the status>',
1963- 'fix-version' : '<the version in which the issue was fixed, if applicable>',
1964- 'id_base' : a base for the integer section of the OVAL id,
1965- 'source-note' : a note about the datasource for debugging
1966- }
1967- """
1968+ # Element generators
1969+ def _generate_reference(self, cve: CVE) -> etree.Element:
1970+ reference = etree.Element("reference", attrib={
1971+ "source": "CVE",
1972+ "ref_id": cve.number,
1973+ "ref_url": f'https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve.number}'
1974+ })
1975
1976- if package['status'] == 'fixed' and not package['fix-version']:
1977- self.warn('"{0}" package in {1} is marked fixed, but missing a fix-version. Changing status to vulnerable.'.format(package['name'], package['source-note']))
1978- package['status'] = 'vulnerable'
1979+ return reference
1980
1981- if package['status'] == 'not-applicable':
1982- # if the package status is not-applicable, skip it!
1983- return False
1984- elif package['status'] == 'not-vulnerable':
1985- # if the packaget status is not-vulnerable, skip it!
1986- return False
1987- """
1988- object_id = self.get_package_object_id(package['name'], package['id_base'], 1)
1989+ def prepare_instructions(self, instruction, cve: CVE, product_description, package: Package, fixed_version):
1990+ if "LSN" in cve.number:
1991+ instruction = """\n
1992+ To check your kernel type and Livepatch version, enter this command:
1993
1994- test_title = "Returns true whether or not the '{0}' package exists.".format(package['name'])
1995- test_id = self.get_package_test_id(package['name'], package['id_base'], test_title, object_id, None, 1, 'any_exist')
1996+ canonical-livepatch status"""
1997
1998- package['note'] = package['name'] + package['note']
1999- return {'id': test_id, 'comment': package['note'], 'negate': True}
2000- """
2001- elif package['status'] == 'vulnerable':
2002- object_id = self.get_package_object_id(package['name'], package['binaries'], package['id_base'])
2003+ if not instruction:
2004+ instruction = """\n
2005+ Update Instructions:
2006
2007- test_title = "Does the '{0}' package exist?".format(package['name'])
2008- test_id = self.get_package_test_id(package['name'], package['id_base'], test_title, object_id)
2009+ Run `sudo pro fix {0}` to fix the vulnerability. The problem can be corrected
2010+ by updating your system to the following package versions:""".format(cve)
2011
2012- package['note'] = package['name'] + package['note']
2013- return {'id': test_id, 'comment': package['note']}
2014- elif package['status'] == 'fixed':
2015- object_id = self.get_package_object_id(package['name'], package['binaries'], package['id_base'])
2016+ instruction += '\n\n'
2017+ source_version = package.get_version_to_check(fixed_version)
2018+ for binary_version in package.get_binary_versions(source_version):
2019+ binaries = package.get_binaries(source_version, binary_version)
2020+ for binary in binaries:
2021+ instruction += """{0} - {1}\n""".format(binary, binary_version)
2022
2023- state_id = self.get_package_version_state_id(package['id_base'], package['fix-version'])
2024+ if "LSN" in cve.number:
2025+ instruction += "Livepatch subscription required"
2026+ elif "Long Term" in product_description or "Interim" in product_description:
2027+ instruction += "No subscription required"
2028+ else:
2029+ instruction += product_description
2030
2031- test_title = "Does the '{0}' package exist and is the version less than '{1}'?".format(package['name'], package['fix-version'])
2032- test_id = self.get_package_test_id(package['name'], package['id_base'], test_title, object_id, state_id)
2033+ return instruction
2034
2035- package['note'] = package['name'] + package['note']
2036- return {'id': test_id, 'comment': package['note']}
2037- else:
2038- if package['status'] != 'unknown':
2039- self.warn('"{0}" is not a supported package status. Outputting for "unknown" status.'.format(package['status']))
2040+ def _populate_pkg(self, cve: CVE, package: Package, root_element, main_criteria, cache, fixed_versions) -> None:
2041+ tests = root_element.find("tests")
2042+ objects = root_element.find("objects")
2043+ variables = root_element.find("variables")
2044+ states = root_element.find("states")
2045+ pkg_rel_entry = cve.pkg_rel_entries[str(package)]
2046
2047- if not hasattr(self, 'id_unknown_test'):
2048- self.id_unknown_test = '{0}:tst:10'.format(self.ns)
2049- self.queue_element('test', """
2050- <ind-def:unknown_test id="{0}" check="all" comment="The result of this test is always UNKNOWN." version="1" />\n""".format(self.id_unknown_test))
2051+ source_version = package.get_version_to_check(pkg_rel_entry.fixed_version)
2052+ for binary_version in package.get_binary_versions(source_version):
2053+ binaries = package.get_binaries(source_version, binary_version)
2054+ cache_entry = f'{package.name}-{binary_version}'
2055
2056- package['note'] = package['name'] + package['note']
2057- return {'id': self.id_unknown_test, 'comment': package['note']}
2058+ cache.setdefault(cache_entry, dict(bin_id=None, def_id=None))
2059
2060- # TODO: xml lib
2061- def get_package_object_id(self, name, bin_pkgs, id_base, version=1):
2062- """ create unique object for each package and return its OVAL id """
2063- if not hasattr(self, 'package_objects'):
2064- self.package_objects = {}
2065+ # If version doesn't exist and only one binary_version, they all have the same binaries
2066+ if not package.version_exists(source_version) and package.all_binaries_same_version:
2067+ cache_entry_bin = f'{package.name}-{GENERIC_VERSION}'
2068+ cache.setdefault(cache_entry_bin, dict(bin_id=None, def_id=None))
2069+ else:
2070+ cache_entry_bin = cache_entry
2071
2072- key = tuple(sorted(bin_pkgs))
2073+ if pkg_rel_entry.status == 'vulnerable' and not self.fixed_only:
2074+ if not cache_entry in cache or not cache[cache_entry]['def_id']:
2075+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, main_criteria)
2076
2077- if key not in self.package_objects:
2078- object_id = '{0}:obj:{1}0'.format(self.ns, id_base)
2079+ test, object, var = self._generate_vulnerable_elements(package, binaries, cache[cache_entry_bin]['bin_id'])
2080+ tests.append(test)
2081
2082- if len(bin_pkgs) > 1:
2083- # create variable for binary package names
2084- variable_id = '{0}:var:{1}0'.format(self.ns, id_base)
2085- if self.oval_format == 'dpkg':
2086- variable_values = '</value>\n <value>'.join(bin_pkgs)
2087- self.queue_element('variable', """
2088- <constant_variable id="{0}" version="{1}" datatype="string" comment="'{2}' package binaries">
2089- <value>{3}</value>
2090- </constant_variable>\n""".format(variable_id, version, name, variable_values))
2091-
2092- # create an object that references the variable
2093- self.queue_element('object', """
2094- <linux-def:dpkginfo_object id="{0}" version="{1}" comment="The '{2}' package binaries.">
2095- <linux-def:name var_ref="{3}" var_check="at least one" />
2096- </linux-def:dpkginfo_object>\n""".format(object_id, version, name, variable_id))
2097+ if not cache[cache_entry_bin]['bin_id']:
2098+ objects.append(object)
2099+ variables.append(var)
2100+ cache[cache_entry_bin]['bin_id'] = self.definition_id
2101
2102+ cache[cache_entry]['def_id'] = self.definition_id
2103+ self._increase_id(is_definition=False)
2104 else:
2105- variable_values = '(?::\w+|)\s+(.*)$</value>\n <value>^'.join(bin_pkgs)
2106- self.queue_element('variable', """
2107- <constant_variable id="{0}" version="{1}" datatype="string" comment="'{2}' package binaries">
2108- <value>^{3}(?::\w+|)\s+(.*)$</value>
2109- </constant_variable>\n""".format(variable_id, version, name, variable_values))
2110-
2111- # create an object that references the variable
2112- self.queue_element('object', """
2113- <ind-def:textfilecontent54_object id="{0}" version="{1}" comment="The '{2}' package binaries.">
2114- <ind-def:path>.</ind-def:path>
2115- <ind-def:filename>manifest</ind-def:filename>
2116- <ind-def:pattern operation="pattern match" datatype="string" var_ref="{3}" var_check="at least one" />
2117- <ind-def:instance operation="greater than or equal" datatype="int">1</ind-def:instance>
2118- </ind-def:textfilecontent54_object>\n""".format(object_id, version, name, variable_id))
2119-
2120- else:
2121- if self.oval_format == 'dpkg':
2122- # 1 binary package, so just use name in object (no variable)
2123- self.queue_element('object', """
2124- <linux-def:dpkginfo_object id="{0}" version="{1}" comment="The '{2}' package binary.">
2125- <linux-def:name>{3}</linux-def:name>
2126- </linux-def:dpkginfo_object>\n""".format(object_id, version, name, bin_pkgs[0]))
2127+ self._add_criterion(cache[cache_entry]['def_id'], pkg_rel_entry, cve, main_criteria)
2128+ elif pkg_rel_entry.status == 'fixed':
2129+ if binary_version in fixed_versions:
2130+ self._add_criterion(fixed_versions[binary_version], pkg_rel_entry, cve, main_criteria)
2131 else:
2132- variable_id = '{0}:var:{1}0'.format(self.ns, id_base)
2133- variable_values = '(?::\w+|)\s+(.*)$</value>\n <value>^'.join(bin_pkgs)
2134- self.queue_element('variable', """
2135- <constant_variable id="{0}" version="{1}" datatype="string" comment="'{2}' package binaries">
2136- <value>^{3}(?::\w+|)\s+(.*)$</value>
2137- </constant_variable>\n""".format(variable_id, version, name, variable_values))
2138- self.queue_element('object', """
2139- <ind-def:textfilecontent54_object id="{0}" version="{1}" comment="The '{2}' package binary.">
2140- <ind-def:path>.</ind-def:path>
2141- <ind-def:filename>manifest</ind-def:filename>
2142- <ind-def:pattern operation="pattern match" datatype="string" var_ref="{3}" var_check="at least one" />
2143- <ind-def:instance operation="greater than or equal" datatype="int">1</ind-def:instance>
2144- </ind-def:textfilecontent54_object>\n""".format(object_id, version, name, variable_id))
2145-
2146- self.package_objects[key] = object_id
2147-
2148- return self.package_objects[key]
2149-
2150- # TODO: xml lib
2151- def get_package_version_state_id(self, id_base, fix_version, version=1):
2152- """ create unique states for each version and return its OVAL id """
2153- if not hasattr(self, 'package_version_states'):
2154- self.package_version_states = {}
2155-
2156- key = fix_version
2157- if key not in self.package_version_states:
2158- state_id = '{0}:ste:{1}0'.format(self.ns, id_base)
2159- if self.oval_format == 'dpkg':
2160- epoch_fix_version = fix_version if fix_version.find(':') != -1 else "0:" + fix_version
2161- self.queue_element('state', """
2162- <linux-def:dpkginfo_state id="{0}" version="{1}" comment="The package version is less than '{2}'.">
2163- <linux-def:evr datatype="debian_evr_string" operation="less than">{2}</linux-def:evr>
2164- </linux-def:dpkginfo_state>\n""".format(state_id, version, epoch_fix_version))
2165- else:
2166- self.queue_element('state', """
2167- <ind-def:textfilecontent54_state id="{0}" version="{1}" comment="The package version is less than '{2}'.">
2168- <ind-def:subexpression datatype="debian_evr_string" operation="less than">{2}</ind-def:subexpression>
2169- </ind-def:textfilecontent54_state>\n""".format(state_id, version, fix_version))
2170- self.package_version_states[key] = state_id
2171+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, main_criteria)
2172
2173- return self.package_version_states[key]
2174+ test, object, var, state = self._generate_fixed_elements(package, binaries, binary_version, cache[cache_entry_bin]['bin_id'])
2175+ tests.append(test)
2176+ states.append(state)
2177
2178- # TODO: xml lib
2179- def get_package_test_id(self, name, id_base, test_title, object_id, state_id=None, version=1, check_existence='at_least_one_exists'):
2180- """ create unique test for each parameter set and return its OVAL id """
2181- if not hasattr(self, 'package_tests'):
2182- self.package_tests = {}
2183-
2184- key = (name, test_title, object_id, state_id)
2185- if key not in self.package_tests:
2186- test_id = '{0}:tst:{1}0'.format(self.ns, id_base)
2187- if self.oval_format == 'dpkg':
2188- state_ref = '\n <linux-def:state state_ref="{0}" />'.format(state_id) if state_id else ''
2189- self.queue_element('test', """
2190- <linux-def:dpkginfo_test id="{0}" version="{1}" check_existence="{5}" check="at least one" comment="{2}">
2191- <linux-def:object object_ref="{3}"/>{4}
2192- </linux-def:dpkginfo_test>\n""".format(test_id, version, test_title, object_id, state_ref, check_existence))
2193+ if not cache[cache_entry_bin]['bin_id']:
2194+ objects.append(object)
2195+ variables.append(var)
2196+ cache[cache_entry_bin]['bin_id'] = self.definition_id
2197+
2198+ fixed_versions[binary_version] = self.definition_id
2199+ self._increase_id(is_definition=False)
2200+
2201+ def _populate_kernel_pkg(self, cve: CVE, package: Package, root_element, main_criteria, running_kernel_id, cache, fixed_versions) -> None:
2202+ # Kernel binaries have all same version
2203+ version = package.get_latest_version()
2204+ binaries = package.get_binaries(version, version)
2205+ pkg_rel_entry = cve.pkg_rel_entries[str(package)]
2206+ cache_entry = f'{package.name}-{pkg_rel_entry.fixed_version}'
2207+
2208+ if not cache_entry in cache:
2209+ # Generate one-time elements
2210+ kernel_criterion = self._generate_kernel_package_elements(package, binaries, root_element, running_kernel_id)
2211+ cache[cache_entry] = kernel_criterion
2212+
2213+ if pkg_rel_entry.status == 'fixed':
2214+ criteria = self._generate_criteria_kernel('AND')
2215+ self._add_to_criteria(criteria, cache[cache_entry], operator='AND', depth=0)
2216+
2217+ kernel_version_criterion = self._add_kernel_elements(cve, package, pkg_rel_entry.fixed_version, pkg_rel_entry, root_element, running_kernel_id, fixed_versions)
2218+ self._add_to_criteria(criteria, kernel_version_criterion, depth=0)
2219+ self._add_to_criteria(main_criteria, criteria, depth=2, operator='OR')
2220+ self._increase_id(is_definition=False)
2221+ else:
2222+ self._add_to_criteria(main_criteria, cache[cache_entry], depth=2, operator='OR')
2223+
2224+ def _generate_elements_from_cve(self, cve, supported_releases, root_element, running_kernel_id, pkg_cache, fixed_versions) -> None:
2225+ if not cve.pkgs: return
2226+ added = False
2227+ definition_element = self._generate_definition_object(cve)
2228+ instructions = ''
2229+ pkgs = cve.get_pkgs(supported_releases)
2230+ for pkg in pkgs:
2231+ if not pkg.versions_binaries: continue
2232+ if not pkg.get_binary_versions(next(iter(pkg.versions_binaries))): continue
2233+ if self._ignore_source_package(pkg.name): continue
2234+
2235+ added = True
2236+ pkg_rel_entry = cve.pkg_rel_entries[str(pkg)]
2237+ if pkg.is_kernel_pkg and self.oval_format != 'oci':
2238+ self._populate_kernel_pkg(cve, pkg, root_element, definition_element, running_kernel_id, pkg_cache, fixed_versions)
2239 else:
2240- state_ref = '\n <ind-def:state state_ref="{0}" />'.format(state_id) if state_id else ''
2241- self.queue_element('test', """
2242- <ind-def:textfilecontent54_test id="{0}" version="{1}" check_existence="{5}" check="at least one" comment="{2}">
2243- <ind-def:object object_ref="{3}"/>{4}
2244- </ind-def:textfilecontent54_test>\n""".format(test_id, version, test_title, object_id, state_ref, check_existence))
2245- self.package_tests[key] = test_id
2246-
2247- return self.package_tests[key]
2248-
2249- def get_running_kernel_testref(self, package):
2250- if package['status'] == 'not-applicable':
2251- # if the package status is not-applicable, skip it!
2252- return
2253- elif package['status'] == 'not-vulnerable':
2254- # if the packaget status is not-vulnerable, skip it!
2255- return
2256-
2257- testref = []
2258- uname_regex = process_kernel_binaries(package['binaries'], self.oval_format)
2259- if uname_regex:
2260- if self.oval_format == 'dpkg':
2261- var_id = self.get_running_kernel_variable_id(
2262- uname_regex,
2263- package['id_base'])
2264- ste_id = self.get_running_kernel_state_id(
2265- uname_regex,
2266- package['id_base'],
2267- var_id)
2268- obj_id = self.get_running_kernel_object_id(
2269- package['id_base'])
2270- test_id = self.get_running_kernel_test_id(
2271- uname_regex, package['id_base'], package['name'],
2272- obj_id, ste_id)
2273- testref.append({'id': test_id,
2274- 'comment': 'Is kernel {0} running'.format(package['name']),
2275- 'kernel': uname_regex,
2276- 'var_id': var_id,
2277- }
2278- )
2279-
2280- # even if a cve was not fixed, we should add the test and object
2281- # but not the state as there won't be a fixed version to compare
2282- # with
2283- ste_id = None
2284- if package['fix-version']:
2285- ste_id = self.get_patched_kernel_state_id(
2286- package['id_base'],
2287- package['fix-version']
2288- )
2289-
2290- obj_id = self.get_patched_kernel_object_id(package['id_base'],
2291- var_id)
2292- test_id = self.get_patched_kernel_test_id(
2293- package['id_base'],
2294- package['fix-version'],
2295- obj_id, ste_id
2296- )
2297- testref.append({'id': test_id,
2298- 'comment': 'kernel version comparison',
2299- 'kernelobj': True})
2300- else: # OCI
2301- object_id = self.get_package_object_id(package['name'],
2302- [uname_regex],
2303- package['id_base'])
2304- state_id = None
2305- test_title = "Does the '{0}' package exist?".format(package['name'])
2306- if package['fix-version']:
2307- state_id = self.get_package_version_state_id(package['id_base'],
2308- package['fix-version'])
2309- test_title = "Does the '{0}' package exist and is the version less than '{1}'?".format(package['name'],
2310- package['fix-version'])
2311- test_id = self.get_package_test_id(package['name'],
2312- package['id_base'],
2313- test_title,
2314- object_id,
2315- state_id)
2316- package['note'] = package['name'] + package['note']
2317- return [{'id': test_id, 'comment': package['note']}]
2318-
2319- return testref
2320-
2321- # TODO: xml lib
2322- def get_running_kernel_object_id(self, id_base, version=1):
2323- """ creates a uname_object so we can use the value from uname -r for
2324- mainly two things:
2325- 1. compare with the return uname is of the same version and flavour
2326- as the kernel we fixed a CVE. This is done in
2327- get_running_kernel_state_id
2328- 2. store the uname value, minus the flavour, in a debian evr string
2329- format, e.g: 0:5.4.0-1059. With this we can compare if the patched
2330- kernel is greater than the running kernel
2331- The result of this two will go through an AND logic to confirm
2332- if we are or not vulnerable to such CVE"""
2333- if not hasattr(self, 'kernel_uname_obj_id'):
2334- self.kernel_uname_obj_id = None
2335-
2336- if not self.kernel_uname_obj_id:
2337- object_id = '{0}:obj:{1}0'.format(self.ns, id_base)
2338-
2339- self.queue_element('object', """
2340- <unix-def:uname_object id="{0}" version="{1}"/>\n""".format(object_id, version))
2341-
2342- self.kernel_uname_obj_id = object_id
2343-
2344- return self.kernel_uname_obj_id
2345-
2346- # TODO: xml lib
2347- def get_running_kernel_state_id(self, uname_regex, id_base, var_id, version=1):
2348- """ create uname_state to compare the system uname to the affected kernel
2349- uname regex, allowing us to verify we are running the same major version
2350- and flavour as the affected kernel.
2351- Return its OVAL id
2352- """
2353- if not hasattr(self, 'uname_states'):
2354- self.uname_states = {}
2355-
2356- if uname_regex not in self.uname_states:
2357- state_id = '{0}:ste:{1}0'.format(self.ns, id_base)
2358- self.queue_element('state', """
2359- <unix-def:uname_state id="{0}" version="{1}">
2360- <unix-def:os_release operation="pattern match">{2}</unix-def:os_release>
2361- </unix-def:uname_state>\n""".format(state_id, version, uname_regex))
2362-
2363- self.uname_states[uname_regex] = state_id
2364-
2365- return self.uname_states[uname_regex]
2366-
2367- # TODO: xml lib
2368- def get_running_kernel_variable_id(self, uname_regex, id_base, version=1):
2369- """ creates a local variable to store running kernel version in devian evr string"""
2370- if not hasattr(self, 'uname_variables'):
2371- self.uname_variables = {}
2372-
2373- var_id = '{0}:var:{1}0'.format(self.ns, id_base)
2374- obj_id = '{0}:obj:{1}0'.format(self.ns, id_base)
2375- self.queue_element('variable', """
2376- <local_variable id="{0}" datatype="debian_evr_string" version="{1}" comment="kernel version in evr format">
2377- <concat>
2378- <literal_component>0:</literal_component>
2379- <regex_capture pattern="^([\d|\.]+-\d+)[-|\w]+$">
2380- <object_component object_ref="{2}" item_field="os_release" />
2381- </regex_capture>
2382- </concat>
2383- </local_variable>\n""".format(var_id, version, obj_id))
2384-
2385- self.uname_variables['local_variable'] = var_id
2386-
2387- return self.uname_variables['local_variable']
2388-
2389- # TODO: xml lib
2390- def get_running_kernel_test_id(self, uname_regex, id_base, name, object_id, state_id, version=1):
2391- """ create uname test and return its OVAL id """
2392- if not hasattr(self, 'uname_tests'):
2393- self.uname_tests = {}
2394-
2395- if uname_regex not in self.uname_tests:
2396- test_id = '{0}:tst:{1}0'.format(self.ns, id_base)
2397- self.queue_element('test', """
2398- <unix-def:uname_test check="at least one" comment="Is kernel {0} currently running?" id="{1}" version="{2}">
2399- <unix-def:object object_ref="{3}"/>
2400- <unix-def:state state_ref="{4}"/>
2401- </unix-def:uname_test>\n""".format(name, test_id, version, object_id, state_id))
2402-
2403- self.uname_tests[uname_regex] = test_id
2404-
2405- return self.uname_tests[uname_regex]
2406+ self._populate_pkg(cve, pkg, root_element, definition_element, pkg_cache, fixed_versions)
2407+
2408+ if pkg_rel_entry.status == 'fixed' and pkg.versions_binaries:
2409+ product_description = cve_lib.get_subproject_description(pkg_rel_entry.release)
2410+ instructions = self.prepare_instructions(instructions, cve, product_description, pkg, pkg_rel_entry.fixed_version)
2411+
2412+ if added:
2413+ definitions = root_element.find("definitions")
2414+ metadata = definition_element.find('metadata')
2415+ metadata.find('description').text = metadata.find('description').text + instructions
2416+ definitions.append(definition_element)
2417
2418- def get_patched_kernel_variable_id(self, id_base, fixed_version, version=1):
2419- """ creates a local variable to store the patched kernel version """
2420- if not hasattr(self, 'patched_kernel_variables'):
2421- self.patched_kernel_variables = {}
2422
2423- patched = re.search('([\d|\.]+-\d+)[\.|\d]+', fixed_version)
2424- if patched:
2425- patched = patched.group(1)
2426- else:
2427- patched = fixed_version
2428-
2429- if patched not in self.patched_kernel_variables:
2430- var_id = '{0}:var:{1}0'.format(self.ns, id_base + 1)
2431+ def generate_oval(self) -> None:
2432+ for release in self.releases:
2433+ self._init_ids(release)
2434+ self.definition_step = 1 * 10 ** 7
2435+ xml_tree, root_element = self._get_root_element()
2436+ generator = self._get_generator("CVE")
2437+ root_element.append(generator)
2438+ self._add_structure(root_element)
2439+ running_kernel_id = None
2440
2441- self.queue_element('variable', """
2442- <constant_variable id="{0}" version="{1}" datatype="debian_evr_string" comment="patched kernel">
2443- <value>0:{2}</value>
2444- </constant_variable>""".format(var_id, version, patched))
2445+ if self.oval_format == 'dpkg':
2446+ # One time kernel check
2447+ self._add_release_checks(root_element)
2448+ self._add_running_kernel_checks(root_element)
2449+ running_kernel_id = self.definition_id
2450+
2451+ pkg_cache = {}
2452+ fixed_versions = {}
2453+ accepted_releases = list(self.parent_releases)
2454+ accepted_releases.insert(0, self.release)
2455+
2456+ all_cves = self.cves[self.release]
2457+ for parent_release in list(self.parent_releases):
2458+ for cve in self.cves[parent_release]:
2459+ if cve not in all_cves:
2460+ all_cves[cve] = self.cves[parent_release][cve]
2461+
2462+ all_cves = dict(sorted(all_cves.items()))
2463+
2464+ for cve in all_cves:
2465+ self._set_definition_id(cve_id=all_cves[cve].number)
2466+ self._generate_elements_from_cve(all_cves[cve], accepted_releases, root_element, running_kernel_id, pkg_cache, fixed_versions)
2467+
2468+ self._write_oval_xml(xml_tree, root_element)
2469+
2470+class OvalGeneratorUSNs(OvalGenerator):
2471+ def __init__(self, release, release_name, cve_paths, packages, progress, pkg_cache, usn_db_dir, fixed_only=True, cve_cache=None, cve_prefix_dir=None, outdir='./', oval_format='dpkg') -> None:
2472+ super().__init__('usn', release, release_name, cve_paths, packages, progress, pkg_cache, fixed_only, cve_cache, cve_prefix_dir, outdir, oval_format)
2473+ self._load_usns(usn_db_dir)
2474+
2475+ def _load_usns(self, usn_db_dir):
2476+ self.usns = {}
2477+ for filename in glob.glob(os.path.join(usn_db_dir, 'database*.json')):
2478+ with open(filename, 'r') as f:
2479+ data = json.load(f)
2480+ for item in data:
2481+ usn = USN(item)
2482+ self.usns[usn.id] = usn
2483+
2484+ for usn_id in sorted(self.usns.keys()):
2485+ if re.search(r'^[0-9]+-[0-9]$', usn_id):
2486+ self.usns[usn_id]['id'] = 'USN-' + usn_id
2487+
2488+ def _generate_advisory(self, usn: USN) -> etree.Element:
2489+ severities = ['low', 'medium', 'high', 'critical']
2490+ advisory = etree.Element("advisory")
2491+ severity = etree.SubElement(advisory, "severity")
2492+ issued = etree.SubElement(advisory, "issued")
2493+ severity = None
2494+ for cve in usn.cves:
2495+ cve_obj = self._generate_cve_tag(self.cves[cve])
2496+ advisory.append(cve_obj)
2497
2498- self.patched_kernel_variables[patched] = var_id
2499+ if not severity or severities.index(self.cves[cve].severity) > severities.index(severity):
2500+ severity = self.cves[cve].severity
2501
2502- return self.patched_kernel_variables[patched]
2503+ severity.text = severity.capitalize()
2504+ issued.text = usn.timestamp
2505
2506- def get_patched_kernel_object_id(self, id_base, var_id, version=1):
2507- """ create variable object that points to kernel version
2508- in evr format in local_variable
2509- """
2510+ return advisory
2511
2512- object_id = '{0}:obj:{1}0'.format(self.ns, id_base + 1)
2513+ def _generate_metadata(self, usn: USN) -> etree.Element:
2514+ metadata = etree.Element("metadata")
2515+ title = etree.SubElement(metadata, "title")
2516+ description = etree.SubElement(metadata, "description")
2517+ affected = etree.SubElement(metadata, "affected", attrib = {"family": "unix"})
2518+ platform = etree.SubElement(affected, "platform")
2519
2520- self.queue_element('object', """
2521- <ind-def:variable_object id="{0}" version="{1}">
2522- <ind-def:var_ref>{2}</ind-def:var_ref>
2523- </ind-def:variable_object>\n""".format(object_id, version, var_id))
2524+ reference = self._generate_reference(usn)
2525+ metadata.append(reference)
2526+ advisory = self._generate_advisory(usn)
2527+ metadata.append(reference)
2528+ metadata.append(advisory)
2529
2530- return object_id
2531+ platform.text = self.release_name
2532+ title.text = usn.title
2533+ description.text = usn.description
2534
2535- # TODO: xml lib
2536- def get_patched_kernel_state_id(self, id_base, fixed_version, version=1):
2537- """ create state to compare to the running kernel
2538- Return its OVAL id
2539- """
2540- if not hasattr(self, 'patched_kernel_states'):
2541- self.patched_kernel_states = {}
2542+ return metadata
2543
2544- patched = re.search('([\d|\.]+-\d+)[\.|\d]+', fixed_version)
2545- if patched:
2546- patched = patched.group(1)
2547- else:
2548- patched = fixed_version
2549+ # Element generators
2550+ def _generate_reference(self, usn: USN) -> etree.Element:
2551+ reference = etree.Element("reference", attrib={
2552+ "source": "USN",
2553+ "ref_id": usn.id,
2554+ "ref_url": f'https://ubuntu.com/security/notices/{usn.id}'
2555+ })
2556
2557- if patched not in self.patched_kernel_states:
2558- state_id = '{0}:ste:{1}0'.format(self.ns, id_base + 1)
2559+ return reference
2560
2561- self.queue_element('state', """
2562- <ind-def:variable_state id="{0}" version="{1}">
2563- <ind-def:value datatype="debian_evr_string" operation="less than">{2}</ind-def:value>
2564- </ind-def:variable_state>\n""".format(state_id, version, patched))
2565+ def _populate_pkg(self, cve: CVE, package: Package, root_element, main_criteria, cache, fixed_versions) -> None:
2566+ tests = root_element.find("tests")
2567+ objects = root_element.find("objects")
2568+ variables = root_element.find("variables")
2569+ states = root_element.find("states")
2570+ pkg_rel_entry = cve.pkg_rel_entries[str(package)]
2571+ cache.setdefault(package.name, dict(bin_id=None, def_id=None))
2572
2573- self.patched_kernel_states[patched] = state_id
2574
2575- return self.patched_kernel_states[patched]
2576+ if pkg_rel_entry.status == 'vulnerable' and not self.fixed_only:
2577+ if not package.name in cache or not cache[package.name]['def_id']:
2578+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, main_criteria)
2579
2580- def get_patched_kernel_test_id(self, id_base, fixed_version, object_id, state_id, version=1):
2581- """ create patched kernel test and return its OVAL id """
2582- if not hasattr(self, 'patched_kernel_tests'):
2583- self.patched_kernel_tests = {}
2584+ test, object, var = self._generate_vulnerable_elements(package, cache[package.name]['bin_id'])
2585+ tests.append(test)
2586
2587- if fixed_version not in self.patched_kernel_tests:
2588- test_id = '{0}:tst:{1}0'.format(self.ns, id_base + 1)
2589+ if not cache[package.name]['bin_id']:
2590+ objects.append(object)
2591+ variables.append(var)
2592+ cache[package.name]['bin_id'] = self.definition_id
2593
2594- if state_id:
2595- self.queue_element('test', """
2596- <ind-def:variable_test id="{0}" version="1" check="all" check_existence="all_exist" comment="kernel version comparison">
2597- <ind-def:object object_ref="{1}"/>
2598- <ind-def:state state_ref="{2}"/>
2599- </ind-def:variable_test>\n""".format(test_id, object_id, state_id))
2600+ cache[package.name]['def_id'] = self.definition_id
2601+ self._increase_id(is_definition=False)
2602 else:
2603- self.queue_element('test', """
2604- <ind-def:variable_test id="{0}" version="1" check="all" check_existence="all_exist" comment="kernel version comparison">
2605- <ind-def:object object_ref="{1}"/>
2606- </ind-def:variable_test>\n""".format(test_id, object_id))
2607-
2608- self.patched_kernel_tests[fixed_version] = test_id
2609-
2610- return self.patched_kernel_tests[fixed_version]
2611-
2612- def queue_element(self, element, xml):
2613- """ add an OVAL element to an output queue file """
2614- if element not in OvalGenerator.supported_oval_elements:
2615- self.warn('"{0}" is not a supported OVAL element.'.format(element))
2616- return
2617-
2618- if not hasattr(self, 'tmp'):
2619- self.tmp = {}
2620- self.tmp_n = random.randrange(1000000, 9999999)
2621+ self._add_criterion(cache[package.name]['def_id'], pkg_rel_entry, cve, main_criteria)
2622+ elif pkg_rel_entry.status == 'fixed':
2623+ if pkg_rel_entry.fixed_version in fixed_versions:
2624+ self._add_criterion(fixed_versions[pkg_rel_entry.fixed_version], pkg_rel_entry, cve, main_criteria)
2625+ else:
2626+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, main_criteria)
2627
2628- if element not in self.tmp:
2629- self.tmp[element] = _open(os.path.join(self.tmpdir,
2630- './queue.{0}.{1}.xml'.format(
2631- self.tmp_n, element)), 'wt')
2632+ test, object, var, state = self._generate_fixed_elements(package, pkg_rel_entry, cache[package.name]['bin_id'])
2633+ tests.append(test)
2634+ states.append(state)
2635
2636- # trim and fix indenting (assumes fragment is nicely indented internally)
2637- xml = xml.strip('\n')
2638- base_indent = re.match(r'\s*', xml).group(0)
2639- xml = re.sub('^{0}'.format(base_indent), ' ', xml, 0,
2640- re.MULTILINE)
2641+ if not cache[package.name]['bin_id']:
2642+ objects.append(object)
2643+ variables.append(var)
2644+ cache[package.name]['bin_id'] = self.definition_id
2645
2646- self.tmp[element].write(xml + '\n')
2647+ fixed_versions[pkg_rel_entry.fixed_version] = self.definition_id
2648+ self._increase_id(is_definition=False)
2649
2650- # TODO: xml lib
2651- def write_to_file(self):
2652- """ dequeue all elements into one OVAL definitions file and clean up """
2653- if not hasattr(self, 'tmp'):
2654- return
2655+ def _populate_kernel_pkg(self, cve: CVE, package: Package, root_element, main_criteria, running_kernel_id, cache, fixed_versions) -> None:
2656+ if not package.name in cache:
2657+ # Generate one-time elements
2658+ kernel_criterion = self._generate_kernel_package_elements(package, root_element, running_kernel_id)
2659+ cache[package.name] = kernel_criterion
2660
2661- # close queue files for writing and then open for reading
2662- for key in self.tmp:
2663- self.tmp[key].close()
2664- self.tmp[key] = _open(self.tmp[key].name, 'rt')
2665+ pkg_rel_entry = cve.pkg_rel_entries[str(package)]
2666
2667- tmp = os.path.join(self.tmpdir, self.output_filepath)
2668- with _open(tmp, 'wt') as f:
2669- # add header
2670- oval_timestamp = datetime.now(tz=timezone.utc).strftime(
2671- '%Y-%m-%dT%H:%M:%S')
2672- f.write("""<oval_definitions
2673- xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5"
2674- xmlns:ind-def="http://oval.mitre.org/XMLSchema/oval-definitions-5#independent"
2675- xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5"
2676- xmlns:unix-def="http://oval.mitre.org/XMLSchema/oval-definitions-5#unix"
2677- xmlns:linux-def="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux"
2678- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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">
2679+ if pkg_rel_entry.status == 'fixed':
2680+ criteria = self._generate_criteria_kernel('AND')
2681+ self._add_to_criteria(criteria, cache[package.name], operator='AND', depth=0)
2682
2683- <generator>
2684- <oval:product_name>Canonical CVE OVAL Generator</oval:product_name>
2685- <oval:product_version>{0}</oval:product_version>
2686- <oval:schema_version>{1}</oval:schema_version>
2687- <oval:timestamp>{2}</oval:timestamp>
2688- </generator>\n""".format(OvalGenerator.generator_version, OvalGenerator.oval_schema_version, oval_timestamp))
2689+ kernel_version_criterion = self._add_kernel_elements(cve, package, pkg_rel_entry, root_element, running_kernel_id, fixed_versions)
2690+ self._add_to_criteria(criteria, kernel_version_criterion, depth=0)
2691+ self._add_to_criteria(main_criteria, criteria, depth=2, operator='OR')
2692+ self._increase_id(is_definition=False)
2693+ else:
2694+ self._add_to_criteria(main_criteria, cache[package.name], depth=2, operator='OR')
2695+
2696+ self._increase_id(is_definition=True)
2697
2698- # add queued file content
2699- for element in OvalGenerator.supported_oval_elements:
2700- if element in self.tmp:
2701- f.write("\n <{0}s>\n".format(element))
2702- f.write(self.tmp[element].read().rstrip())
2703- f.write("\n </{0}s>".format(element))
2704+ def generate_oval(self) -> None:
2705+ self._reset()
2706+ xml_tree, root_element = self._get_root_element()
2707+ generator = self._get_generator("USN")
2708+ root_element.append(generator)
2709+ self._add_structure(root_element)
2710
2711- # add footer
2712- f.write("\n</oval_definitions>")
2713+ if self.oval_format == 'dpkg':
2714+ # One time kernel check
2715+ self._add_release_checks(root_element)
2716+ self._add_running_kernel_checks(root_element)
2717+ running_kernel_id = self.definition_id
2718+ self._increase_id(is_definition=True)
2719
2720- # close and delete queue files
2721- for key in self.tmp:
2722- self.tmp[key].close()
2723- os.remove(self.tmp[key].name)
2724+ definitions = root_element.find("definitions")
2725+ pkg_cache = {}
2726+ fixed_versions = {}
2727
2728- # close self.output_filepath and move into place
2729- f.close()
2730- shutil.move(tmp, os.path.join(self.output_dir, self.output_filepath))
2731+ for usn in self.usns:
2732+ definition_element = self._generate_definition_object(self.usns[usn])
2733+ instructions = ''
2734
2735- # remove tmp dir if empty
2736- if not os.listdir(self.tmpdir):
2737- os.rmdir(self.tmpdir)
2738+ for cve in self.cves:
2739+ for pkg in self.cves[cve].pkgs:
2740+ pkg_rel_entry = self.cves[cve].pkg_rel_entries[str(pkg)]
2741+ if self.packages[pkg].is_kernel_pkg and self.oval_format != 'oci':
2742+ self._populate_kernel_pkg(self.cves[cve], pkg, root_element, definition_element, running_kernel_id, pkg_cache, fixed_versions)
2743+ else:
2744+ self._populate_pkg(self.cves[cve], pkg, root_element, definition_element, pkg_cache, fixed_versions)
2745+
2746+ if pkg_rel_entry.status == 'fixed' and pkg.binaries:
2747+ product_description = cve_lib.get_subproject_description(pkg_rel_entry.release)
2748+ instructions = prepare_instructions(instructions, self.cves[cve].number, product_description, {'binaries': pkg.binaries, 'fix-version': pkg_rel_entry.fixed_version})
2749+
2750+ metadata = definition_element.find('metadata')
2751+ metadata.find('description').text = metadata.find('description').text + instructions
2752+ definitions.append(definition_element)
2753
2754- def unique_id_base(self, id_base, note):
2755- """ queue a warning message """
2756- if not hasattr(self, 'id_bases'):
2757- self.id_bases = {}
2758- is_unique = id_base not in self.id_bases.keys()
2759- if not is_unique:
2760- self.warn('ID Base collision {0} in {1} and {2}.'.format(
2761- id_base, note, self.id_bases[id_base]))
2762- self.id_bases[id_base] = note
2763- return is_unique
2764-
2765- def warn(self, message):
2766- """ print a warning message """
2767- print('WARNING: {0}'.format(message))
2768+ self._write_oval_xml(xml_tree, root_element)
2769
2770 class OvalGeneratorUSN():
2771 supported_oval_elements = ('definition', 'test', 'object', 'state',
2772@@ -2709,7 +2562,7 @@ class OvalGeneratorUSN():
2773 xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5"
2774 xmlns:unix="http://oval.mitre.org/XMLSchema/oval-definitions-5#unix"
2775 xmlns:linux="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux"
2776- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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">
2777+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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#linux linux-definitions-schema.xsd">
2778
2779 <generator>
2780 <oval:product_name>Canonical USN OVAL Generator</oval:product_name>
2781diff --git a/test/gold_oval_structure/oval.xml b/test/gold_oval_structure/oval.xml
2782index c45fd96..1600b15 100644
2783--- a/test/gold_oval_structure/oval.xml
2784+++ b/test/gold_oval_structure/oval.xml
2785@@ -4,7 +4,7 @@
2786 xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5"
2787 xmlns:unix="http://oval.mitre.org/XMLSchema/oval-definitions-5#unix"
2788 xmlns:linux="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux"
2789- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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">
2790+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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#linux linux-definitions-schema.xsd">
2791
2792 <generator>
2793 <oval:product_name>Canonical USN OVAL Generator</oval:product_name>

Subscribers

People subscribed via source and target branches