Merge ~litios/ubuntu-cve-tracker:oval-refactor into ubuntu-cve-tracker:master
- Git
- lp:~litios/ubuntu-cve-tracker
- oval-refactor
- Merge into master
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eduardo Barretto | Approve | ||
Review via email: mp+455118@code.launchpad.net |
Commit message
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.
Eduardo Barretto (ebarretto) wrote (last edit ): | # |
- 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>
Eduardo Barretto (ebarretto) wrote : | # |
lgtm!
thanks for landing the last fixes!
- 89047d3... by David Fernandez Gonzalez
-
OVAL: improve USN generation code
* Fix bug when not listing releases
* Refactor generate_oval_usn code in generate-ovalSigned-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>
David Fernandez Gonzalez (litios) wrote : | # |
Thanks a lot for all the reviews and the patience to check everything!!
Preview Diff
1 | diff --git a/scripts/generate-oval b/scripts/generate-oval |
2 | index 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() |
250 | diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py |
251 | index 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> |
2781 | diff --git a/test/gold_oval_structure/oval.xml b/test/gold_oval_structure/oval.xml |
2782 | index 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> |
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 :)