Merge ~litios/ubuntu-cve-tracker:json-pkg-gen into ubuntu-cve-tracker:master
- Git
- lp:~litios/ubuntu-cve-tracker
- json-pkg-gen
- Merge into master
Status: | Merged |
---|---|
Merge reported by: | David Fernandez Gonzalez |
Merged at revision: | 319f3ab92105e771b98be04cb0e594b9246e0457 |
Proposed branch: | ~litios/ubuntu-cve-tracker:json-pkg-gen |
Merge into: | ubuntu-cve-tracker:master |
Diff against target: |
1434 lines (+1224/-45) 5 files modified
.launchpad.yaml (+3/-1) scripts/generate-oval (+23/-4) scripts/oval_lib.py (+238/-40) test/json-gen-schema.json (+123/-0) test/test_json_generation.py (+837/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eduardo Barretto | Approve | ||
Review via email: mp+461645@code.launchpad.net |
Commit message
Description of the change
This is the JSON Generator as per https:/
This generator follows the same pattern as the OVAL generators. It reuses the loading capabilities of the main OVALGenerator, but it uses the SSN generator as it also relies on the SSNs being loaded. It also implements the parent support from OVAL.
Generation is done through the generate-oval script, by adding another type `--type json-pkg`.
The output filename is 'com.ubuntu.
Unit testing has been added to test/test_
David Fernandez Gonzalez (litios) wrote : | # |
- 83d24ee... by David Fernandez Gonzalez
-
[JSON] Output the right filename
Signed-off-by: David Fernandez Gonzalez <email address hidden>
- 93f67ba... by David Fernandez Gonzalez
-
[JSON PKG] Account for Packages/USNs/CVEs in parent releases
The objects might not being present in the child release, but
be in the parent. We need to include them.Source packages take a different treatment because what we want
to merge is all the sources.Also, create a shared function to retrieve all objects affecting
all parent releases + selected release.Tests updated to account for parent cases.
Signed-off-by: David Fernandez Gonzalez <email address hidden>
- 114b27e... by David Fernandez Gonzalez
-
[OVAL] Use new expand system
Signed-off-by: David Fernandez Gonzalez <email address hidden>
Eduardo Barretto (ebarretto) wrote : | # |
a few things to fix, nothing major
- 670af96... by David Fernandez Gonzalez
-
[JSON] Styling issues
Signed-off-by: David Fernandez Gonzalez <email address hidden>
David Fernandez Gonzalez (litios) wrote : | # |
Hey Eduardo, thanks for the review. I fixed the issues mentioned in a new commit, except some of them:
* I believe there's an alignment issue here.
- This seems to be already in the current code, I just fixed it.
* I believe we can drop the fixed_only argument. It doesn't fit the schema.
- Since this works from the _load function, we could generate the file only with the CVEs that have been fixed instead of all. We are not going to use it in production, but since it works without really touching anything, I didn't see any benefit in removing it. Let me know if you still think we should get rid of it and I will delete it.
* could we just rely on get_pocket and avoid this check?
- Not right now, because the pocket will be either security, release, etc. They want the pocket to be esm-infra/apps if that's where the version is, which is a custom feature for this format. If we migrate that to get_pocket, this will also be the case for regular OVAL.
- b710fca... by David Fernandez Gonzalez
-
[OVAL] More styling issues
Signed-off-by: David Fernandez Gonzalez <email address hidden>
- ebcfa6f... by David Fernandez Gonzalez
-
[JSON] Account for cases with parent release and /
Signed-off-by: David Fernandez Gonzalez <email address hidden>
- 59fad01... by David Fernandez Gonzalez
-
[JSON] Add JSON schema
Signed-off-by: David Fernandez Gonzalez <email address hidden>
- 2618ee3... by David Fernandez Gonzalez
-
[JSON] Clean spaces in test_json_
generation Signed-off-by: David Fernandez Gonzalez <email address hidden>
- 3dd0c8e... by David Fernandez Gonzalez
-
[JSON] Update schema to the last available
Signed-off-by: David Fernandez Gonzalez <email address hidden>
- 319f3ab... by David Fernandez Gonzalez
-
[JSON] Update tests to match current format
Signed-off-by: David Fernandez Gonzalez <email address hidden>
Eduardo Barretto (ebarretto) wrote : | # |
lgtm, thanks for addressing all the comments!
If possible in a later PR, I think it would be good to validate the generated test json against the schema.
Preview Diff
1 | diff --git a/.launchpad.yaml b/.launchpad.yaml |
2 | index 9bcdb5d..e672a85 100644 |
3 | --- a/.launchpad.yaml |
4 | +++ b/.launchpad.yaml |
5 | @@ -26,6 +26,8 @@ jobs: |
6 | - python3-pytest-cov |
7 | - python3-yaml |
8 | - shellcheck |
9 | + - python3-dateutil |
10 | + - python3-pytest-mock |
11 | run-before: | |
12 | # configure a basic ~/.ubuntu-cve-tracker.conf |
13 | echo plb_authentication=/dev/null > ~/.ubuntu-cve-tracker.conf |
14 | @@ -42,7 +44,7 @@ jobs: |
15 | echo "reporting syntax issues on scripts" |
16 | make check-syntax-scripts |
17 | echo "Running unit tests..." |
18 | - pytest-3 --cov=scripts ./scripts/test_*.py ./test/test_oval_lib_unit.py |
19 | + pytest-3 --cov=scripts ./scripts/test_*.py ./test/test_oval_lib_unit.py ./test/test_json_generation.py |
20 | check-cves: |
21 | series: jammy |
22 | architectures: amd64 |
23 | diff --git a/scripts/generate-oval b/scripts/generate-oval |
24 | index cc44ee7..d01329f 100755 |
25 | --- a/scripts/generate-oval |
26 | +++ b/scripts/generate-oval |
27 | @@ -45,7 +45,7 @@ def main(): |
28 | |
29 | # parse command line options |
30 | parser = argparse.ArgumentParser(description='Generate OVAL for CVE, PKG or USNs.') |
31 | - parser.add_argument('--type', required=True, choices=['cve', 'pkg', 'usn'], |
32 | + parser.add_argument('--type', required=True, choices=['cve', 'pkg', 'usn', 'json-pkg'], |
33 | help='OVAL format') |
34 | parser.add_argument('--cves', nargs='*', default=['active/CVE-*', 'retired/CVE-*'], |
35 | help='pathname patterns (globs) specifying CVE ' |
36 | @@ -104,7 +104,7 @@ def main(): |
37 | for r in set(all_releases).difference(set(eol_releases)).difference(set([devel_release])): |
38 | if needs_oval(r): |
39 | releases.append(r) |
40 | - |
41 | + |
42 | out_releases = releases |
43 | |
44 | # for each release we need to get its parent to also |
45 | @@ -124,8 +124,9 @@ def main(): |
46 | out_releases = set(out_releases) - parent_releases |
47 | |
48 | if args.type == 'usn': |
49 | - generate_oval_usn(args.output_dir, args.usn_number, releases, |
50 | - args.cves, args.usn_db_dir, args.no_progress, args.oci_prefix, args.oci_output_dir) |
51 | + generate_oval_usn(args.output_dir, args.usn_number, releases, |
52 | + args.cves, args.usn_db_dir, args.no_progress, |
53 | + args.oci_prefix, args.oci_output_dir) |
54 | else: |
55 | cache = {} |
56 | for release in releases: |
57 | @@ -136,6 +137,12 @@ def main(): |
58 | generate_oval_package(out_releases, args.output_dir, args.cves, cache, cve_cache, args.oci, args.no_progress, args.packages, args.fixed_only, args.oci_output_dir, args.expand) |
59 | elif args.type == 'cve': |
60 | generate_oval_cve(out_releases, args.output_dir, args.cves, cache, cve_cache, args.oci, args.no_progress, args.packages, args.fixed_only, args.oci_output_dir, args.expand) |
61 | + elif args.type == 'json-pkg': |
62 | + usn_database = get_usn_database(args.usn_db_dir) |
63 | + if not usn_database: |
64 | + error("Error getting local USN database. Please, run '$UCT/scripts/fetch-db database.json.bz2' to retrieve the database and try again.") |
65 | + |
66 | + generate_json_pkg_oval(out_releases, args.output_dir, args.cves, cache, cve_cache, usn_database, args.no_progress, args.packages, args.fixed_only, args.expand) |
67 | |
68 | |
69 | def warn(message): |
70 | @@ -292,5 +299,17 @@ def generate_oval_cve(releases, outdir, cves, pkg_cache, cve_cache, oci, no_prog |
71 | if not no_progress: |
72 | print(f'[X] Done generating OVAL CVE for packages in releases {", ".join(releases)}') |
73 | |
74 | + |
75 | +def generate_json_pkg_oval(releases, output_dir, cves, cache, cve_cache, usn_database, no_progress, packages, fixed_only, expand): |
76 | + if not no_progress: |
77 | + print(f'[*] Generating JSON PKG for packages in releases {", ".join(releases)}') |
78 | + |
79 | + gen = oval_lib.JSONPkgGenerator(releases, cves, packages, not no_progress, cache, usn_database, fixed_only, cve_cache, output_dir, expand) |
80 | + |
81 | + gen.generate_json() |
82 | + |
83 | + if not no_progress: |
84 | + print(f'[X] Done generating JSON PKG for packages in releases {", ".join(releases)}') |
85 | + |
86 | if __name__ == '__main__': |
87 | main() |
88 | diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py |
89 | index 6273d08..7105732 100755 |
90 | --- a/scripts/oval_lib.py |
91 | +++ b/scripts/oval_lib.py |
92 | @@ -29,6 +29,8 @@ import tempfile |
93 | import collections |
94 | import glob |
95 | import xml.etree.cElementTree as etree |
96 | +import json |
97 | +from dateutil import parser, tz |
98 | from xml.dom import minidom |
99 | from typing import Tuple # Needed because of Python < 3.9 and to also support < 3.7 |
100 | |
101 | @@ -374,21 +376,25 @@ class Package: |
102 | self.rel = rel |
103 | self.description = cve_lib.lookup_package_override_description(pkgname) |
104 | |
105 | - if not self.description: |
106 | - if 'description' in sources[rel][pkgname]: |
107 | - self.description = sources[rel][pkgname]['description'] |
108 | - elif pkgname in source_map_binaries[rel] and \ |
109 | - 'description' in source_map_binaries[rel][pkgname]: |
110 | - self.description = source_map_binaries[rel][pkgname]['description'] |
111 | - else: |
112 | - # Get first description found |
113 | - if 'binaries' in sources[self.rel][self.name]: |
114 | - for binary in sources[self.rel][self.name]['binaries']: |
115 | - if binary in source_map_binaries[self.rel] and 'description' in source_map_binaries[self.rel][binary]: |
116 | - self.description = source_map_binaries[self.rel][binary]["description"] |
117 | - break |
118 | - |
119 | - self.section = sources[rel][pkgname]['section'] |
120 | + if rel in sources and pkgname in sources[rel]: |
121 | + if not self.description: |
122 | + if 'description' in sources[rel][pkgname]: |
123 | + self.description = sources[rel][pkgname]['description'] |
124 | + elif pkgname in source_map_binaries[rel] and \ |
125 | + 'description' in source_map_binaries[rel][pkgname]: |
126 | + self.description = source_map_binaries[rel][pkgname]['description'] |
127 | + else: |
128 | + # Get first description found |
129 | + if 'binaries' in sources[self.rel][self.name]: |
130 | + for binary in sources[self.rel][self.name]['binaries']: |
131 | + if binary in source_map_binaries[self.rel] and 'description' in source_map_binaries[self.rel][binary]: |
132 | + self.description = source_map_binaries[self.rel][binary]["description"] |
133 | + break |
134 | + |
135 | + self.section = sources[rel][pkgname]['section'] |
136 | + else: |
137 | + self.section = 'main' |
138 | + |
139 | self.versions_binaries = versions_binaries if versions_binaries else {} |
140 | self.earliest_version = self.get_earliest_version() |
141 | self.latest_version = self.get_latest_version() |
142 | @@ -480,12 +486,14 @@ class Package: |
143 | return self.__str__() |
144 | |
145 | class USN: |
146 | - def __init__(self, data, cve_objs, pkgs_by_rel): |
147 | + def __init__(self, data, cve_objs, pkgs_by_rel, lp_bugs): |
148 | for item in ['description', 'releases', 'title', 'timestamp', 'summary', 'action', 'id', 'isummary']: |
149 | if item in data: |
150 | setattr(self, item, data[item]) |
151 | else: |
152 | setattr(self, item, None) |
153 | + |
154 | + self.lp_bugs = lp_bugs |
155 | self.cves = cve_objs |
156 | self.pkgs = self._generate_pkg_fixed_ver_tuple_dict(pkgs_by_rel) |
157 | |
158 | @@ -498,6 +506,13 @@ class USN: |
159 | tup_dict[rel][src_name] = (pkg_object, fixed_ver) |
160 | return tup_dict |
161 | |
162 | + def get_pkg_fixed_version(self, package: Package): |
163 | + if not self.is_package_present(package): return None |
164 | + return self.pkgs[package.rel][package.name][1] |
165 | + |
166 | + def is_package_present(self, package: Package): |
167 | + return package.rel in self.pkgs and package.name in self.pkgs[package.rel] |
168 | + |
169 | def __str__(self) -> str: |
170 | # return f'description: {self.description}\nid: {self.id}\ncves: {self.cves}\npkgs: {self.pkgs}\n' # TODO: remove this ugly debug print |
171 | return self.id |
172 | @@ -531,14 +546,7 @@ class OvalGenerator: |
173 | self.release_codename = cve_lib.release_progenitor(self.release) if cve_lib.release_progenitor(self.release) else self.release |
174 | self.release_codename = self.release_codename.replace('/', '_') |
175 | self.release_name = cve_lib.release_name(self.release) |
176 | - |
177 | - self.parent_releases = list() |
178 | - current_release = self.release |
179 | - while(cve_lib.release_parent(current_release)): |
180 | - current_release = cve_lib.release_parent(current_release) |
181 | - if current_release != self.release and \ |
182 | - current_release not in self.parent_releases: |
183 | - self.parent_releases.append(current_release) |
184 | + self.parent_releases = self._get_parent_releases(self.release) |
185 | |
186 | self.ns = 'oval:com.ubuntu.{0}'.format(self.release_codename) |
187 | self.id = 100 |
188 | @@ -572,6 +580,25 @@ class OvalGenerator: |
189 | |
190 | return False |
191 | |
192 | + def _get_parent_releases(self, release) -> list[str]: |
193 | + parent_releases = list() |
194 | + current_release = release |
195 | + while(cve_lib.release_parent(current_release)): |
196 | + current_release = cve_lib.release_parent(current_release) |
197 | + if current_release != release and \ |
198 | + current_release not in parent_releases: |
199 | + parent_releases.append(current_release) |
200 | + |
201 | + return parent_releases |
202 | + |
203 | + def _get_objects_including_parents(self, objects) -> dict: |
204 | + all_objs = dict() |
205 | + for parent_release in self.parent_releases[::-1]: |
206 | + all_objs.update(objects[parent_release]) |
207 | + |
208 | + all_objs.update(objects[self.release]) |
209 | + return all_objs |
210 | + |
211 | def _add_structure(self, root) -> None: |
212 | structure = {} |
213 | for element in self.supported_oval_elements: |
214 | @@ -1508,11 +1535,7 @@ class OvalGeneratorPkg(OvalGenerator): |
215 | running_kernel_id = self.definition_id |
216 | self._increase_id(is_definition=True) |
217 | |
218 | - all_pkgs = dict() |
219 | - for parent_release in self.parent_releases[::-1]: |
220 | - all_pkgs.update(self.packages[parent_release]) |
221 | - |
222 | - all_pkgs.update(self.packages[self.release]) |
223 | + all_pkgs = self._get_objects_including_parents(self.packages) |
224 | |
225 | for pkg in all_pkgs: |
226 | if self._ignore_source_package(pkg): continue |
227 | @@ -1760,12 +1783,7 @@ class OvalGeneratorCVE(OvalGenerator): |
228 | accepted_releases = self.parent_releases.copy() |
229 | accepted_releases.insert(0, self.release) |
230 | |
231 | - all_cves = self.cves[self.release] |
232 | - for parent_release in self.parent_releases: |
233 | - for cve in self.cves[parent_release]: |
234 | - if cve not in all_cves: |
235 | - all_cves[cve] = self.cves[parent_release][cve] |
236 | - |
237 | + all_cves = self._get_objects_including_parents(self.cves) |
238 | all_cves = dict(sorted(all_cves.items())) |
239 | |
240 | for cve in all_cves: |
241 | @@ -1775,21 +1793,24 @@ class OvalGeneratorCVE(OvalGenerator): |
242 | self._write_oval_xml(xml_tree, root_element) |
243 | |
244 | class OvalGeneratorUSNs(OvalGenerator): |
245 | - def __init__(self, releases, cve_paths, packages, progress, pkg_cache, usn_database, fixed_only=True, cve_cache=None, outdir='./', oval_format='dpkg') -> None: |
246 | - super().__init__('usn', releases, cve_paths, packages, progress, pkg_cache, fixed_only, cve_cache, outdir, oval_format) |
247 | + def __init__(self, releases, cve_paths, packages, progress, pkg_cache, usn_database, fixed_only=True, cve_cache=None, outdir='./', oval_format='dpkg', expand=False) -> None: |
248 | + super().__init__('usn', releases, cve_paths, packages, progress, pkg_cache, fixed_only, cve_cache, outdir, oval_format, expand) |
249 | self.usns = self._load_usns(usn_database) |
250 | |
251 | def _load_usns(self, usn_database): |
252 | usns = {} |
253 | # go thru every USN in the JSON |
254 | - for usn_id, usn_data in usn_database.items(): |
255 | + for usn_id, usn_data in dict(sorted(usn_database.items())).items(): |
256 | # take existing CVE and Package objects |
257 | cve_objs = {} |
258 | pkg_objs_by_rels = {} |
259 | + lp_bugs = [] |
260 | for rel, info in usn_data['releases'].items(): |
261 | # CVE stays the same across releases |
262 | for cve in usn_data['cves']: |
263 | - if cve_objs.get(cve) is None: |
264 | + if 'launchpad' in cve: |
265 | + lp_bugs.append(cve) |
266 | + elif cve_objs.get(cve) is None: |
267 | try: |
268 | cve_objs[cve] = self.cves[rel][cve] |
269 | except KeyError: |
270 | @@ -1804,7 +1825,7 @@ class OvalGeneratorUSNs(OvalGenerator): |
271 | pkg_objs_by_rels[rel] = pkg_objs |
272 | # create a USN object with fields in the USN and |
273 | # corresponding CVEs and Packages |
274 | - usns[usn_id] = USN(usn_data, cve_objs, pkg_objs_by_rels) |
275 | + usns[usn_id] = USN(usn_data, cve_objs, pkg_objs_by_rels, lp_bugs) |
276 | return usns |
277 | |
278 | def _generate_advisory(self, usn: USN) -> etree.Element: |
279 | @@ -2702,6 +2723,183 @@ class OvalGeneratorUSN(): |
280 | if not os.listdir(self.tmpdir): |
281 | os.rmdir(self.tmpdir) |
282 | |
283 | +class JSONPkgGenerator(OvalGeneratorUSNs): |
284 | + """Class to generate JSON representation of OVAL""" |
285 | + |
286 | + def __init__(self, releases, cve_paths, packages, progress, pkg_cache, usn_database,fixed_only=True, cve_cache=None, outdir='./', expand=False) -> None: |
287 | + self.json_data = dict() |
288 | + super().__init__(releases, cve_paths, packages, progress, pkg_cache, usn_database, fixed_only, cve_cache, outdir, expand=expand) |
289 | + self.generator_type = 'json_pkg' |
290 | + |
291 | + def _generate_usn_info(self, usn: USN): |
292 | + return { |
293 | + 'description': usn.description, |
294 | + 'published_at': datetime.fromtimestamp(usn.timestamp).isoformat(timespec="seconds"), |
295 | + 'related_cves': list(usn.cves.keys()), |
296 | + 'related_launchpad_bugs': usn.lp_bugs |
297 | + } |
298 | + |
299 | + def _generate_usns_info(self): |
300 | + usns_json = {} |
301 | + # USNs should probably follow the same structure as self.packages and self.cves |
302 | + # For now, doing this instead of self._get_objects_including_parents |
303 | + all_releases = self.parent_releases.copy() |
304 | + all_releases.append(self.release) |
305 | + |
306 | + for usn_id, usn in self.usns.items(): |
307 | + affects = False |
308 | + for usn_release in usn.releases: |
309 | + if usn_release in all_releases: |
310 | + affects = True |
311 | + break |
312 | + |
313 | + if not affects: continue |
314 | + |
315 | + usns_json[f'USN-{usn_id}'] = self._generate_usn_info(usn) |
316 | + return usns_json |
317 | + |
318 | + def _generate_cve_info(self, cve: CVE) -> dict: |
319 | + notes = [] |
320 | + |
321 | + for note in cve.notes: |
322 | + notes.append(f'{note[0]}> {note[1]}') |
323 | + |
324 | + return { |
325 | + 'description': cve.description.strip(), |
326 | + 'published_at': parser.parse(cve.public_date, |
327 | + tzinfos={'PDT': tz.gettz('America/Los_Angeles'), |
328 | + 'PST': tz.gettz('America/Los_Angeles') |
329 | + } |
330 | + ).isoformat(timespec="seconds"), |
331 | + 'notes': notes, |
332 | + 'mitigation': cve.mitigation, |
333 | + 'cvss_severity': cve.cvss[0]['baseSeverity'].lower() if cve.cvss else None, |
334 | + 'cvss_score': float(cve.cvss[0]['baseScore']) if cve.cvss else None |
335 | + } |
336 | + |
337 | + def _generate_cves_info(self) -> dict: |
338 | + cves_json = {} |
339 | + for cve_number, cve in self._get_objects_including_parents(self.cves).items(): |
340 | + cves_json[cve_number] = self._generate_cve_info(cve) |
341 | + |
342 | + return cves_json |
343 | + |
344 | + def _generate_cve_pkg_info(self, package: Package) -> dict: |
345 | + pkg_cves = {} |
346 | + for cve in package.cves: |
347 | + cve_pkg_rel_entry = None |
348 | + |
349 | + for pkg_rel_entry_id in cve.pkg_rel_entries: |
350 | + pkg_rel_entry_package, pkg_rel_entry_rel = pkg_rel_entry_id.split('/', maxsplit=1) |
351 | + if pkg_rel_entry_package == package.name and \ |
352 | + pkg_rel_entry_rel == package.rel: |
353 | + cve_pkg_rel_entry = cve.pkg_rel_entries[pkg_rel_entry_id] |
354 | + break |
355 | + |
356 | + if not cve_pkg_rel_entry: |
357 | + print(f'CVE entry for {package.name} - {package.rel} in {cve.number} not found') |
358 | + continue |
359 | + |
360 | + pkg_cves[cve.number] = { |
361 | + 'source_fixed_version': cve_pkg_rel_entry.fixed_version, |
362 | + 'ubuntu_priority': cve.priority, |
363 | + 'status': cve_pkg_rel_entry.status |
364 | + } |
365 | + |
366 | + return pkg_cves |
367 | + |
368 | + def _generate_usn_pkg_info(self, package: Package) -> dict: |
369 | + usns_pkg_info = dict() |
370 | + usns_regression_pkg_info = dict() |
371 | + for usn_id, usn in self.usns.items(): |
372 | + if usn.is_package_present(package): |
373 | + usn_pkg_info = { |
374 | + 'source_fixed_version': usn.get_pkg_fixed_version(package) |
375 | + } |
376 | + |
377 | + if 'regression' in usn.title.lower(): |
378 | + usns_regression_pkg_info[f'USN-{usn_id}'] = usn_pkg_info |
379 | + else: |
380 | + usns_pkg_info[f'USN-{usn_id}'] = usn_pkg_info |
381 | + |
382 | + return usns_pkg_info, usns_regression_pkg_info |
383 | + |
384 | + def _generate_package_source_info(self, package: Package, source_package_info: dict): |
385 | + for source_version in package.versions_binaries: |
386 | + if 'esm' in source_version: |
387 | + pocket = 'esm-apps' if package.section == 'universe' else 'esm-infra' |
388 | + else: |
389 | + _, pocket = get_pocket(self.pkg_cache, package.name, source_version, package.rel) |
390 | + |
391 | + source_package_info.setdefault(source_version, { |
392 | + 'binary_packages': dict(), |
393 | + 'pocket': pocket.lower() |
394 | + }) |
395 | + for binary_version in package.versions_binaries[source_version]: |
396 | + for binary in package.versions_binaries[source_version][binary_version]: |
397 | + source_package_info[source_version]['binary_packages'][binary] = binary_version |
398 | + |
399 | + def _generate_package_info(self, package: Package) -> dict: |
400 | + source_package_info = dict() |
401 | + |
402 | + usns_pkg_info, usns_reg_pkg_info = self._generate_usn_pkg_info(package) |
403 | + self._generate_package_source_info(package, source_package_info) |
404 | + |
405 | + for release in self.parent_releases: |
406 | + if not package.name in self.packages[release]: continue |
407 | + parent_package = self.packages[release][package.name] |
408 | + self._generate_package_source_info(parent_package, source_package_info) |
409 | + |
410 | + return { |
411 | + 'source_versions': source_package_info, |
412 | + 'ubuntu_security_notices': usns_pkg_info, |
413 | + 'ubuntu_security_notices_regressions': usns_reg_pkg_info, |
414 | + 'cves': self._generate_cve_pkg_info(package) |
415 | + } |
416 | + |
417 | + def _generate_packages_info(self) -> dict: |
418 | + json_packages = {} |
419 | + for package_name, package in self._get_objects_including_parents(self.packages).items(): |
420 | + json_packages[package_name] = self._generate_package_info(package) |
421 | + |
422 | + return json_packages |
423 | + |
424 | + def _generate_json_headers(self) -> dict: |
425 | + return { |
426 | + 'format': '1', |
427 | + 'published_at': datetime.now().isoformat(timespec="seconds"), |
428 | + 'release': self.release_codename, |
429 | + 'packages': dict(), |
430 | + 'security_issues': dict() |
431 | + } |
432 | + |
433 | + def _write_json(self, json_structure: dict) -> None: |
434 | + with open(os.path.join(self.output_dir, self.output_file), 'w+') as file: |
435 | + json.dump(json_structure, file) |
436 | + |
437 | + def _generate_structure(self) -> dict: |
438 | + json_structure = self._generate_json_headers() |
439 | + json_structure['packages'] = self._generate_packages_info() |
440 | + json_structure['security_issues']['cves'] = self._generate_cves_info() |
441 | + json_structure['security_issues']['usns'] = self._generate_usns_info() |
442 | + |
443 | + return json_structure |
444 | + |
445 | + def _init_ids(self, release): |
446 | + self.release = release |
447 | + self.release_codename = cve_lib.release_progenitor(release) if cve_lib.release_progenitor(release) else release |
448 | + self.release_codename = self.release_codename.replace('/', '_') |
449 | + self.parent_releases = self._get_parent_releases(release) |
450 | + if self._expand_release(self.release, self.expand): |
451 | + self.output_file = f'com.ubuntu.{self.release.replace("/", "_")}.pkg.json' |
452 | + else: |
453 | + self.output_file = f'com.ubuntu.{self.release_codename}.pkg.json' |
454 | + |
455 | + def generate_json(self) -> None: |
456 | + for release in self.releases: |
457 | + self._init_ids(release) |
458 | + self._write_json(self._generate_structure()) |
459 | + |
460 | def find_release_codename(release): |
461 | return cve_lib.release_progenitor(release) if cve_lib.release_progenitor(release) else release.replace('/', '_') |
462 | |
463 | diff --git a/test/json-gen-schema.json b/test/json-gen-schema.json |
464 | new file mode 100644 |
465 | index 0000000..6d1a833 |
466 | --- /dev/null |
467 | +++ b/test/json-gen-schema.json |
468 | @@ -0,0 +1,123 @@ |
469 | +{ |
470 | + "$schema": "http://json-schema.org/draft/2020-12/schema#", |
471 | + "type": "object", |
472 | + "properties": { |
473 | + "format": {"type": "string"}, |
474 | + "published_at": {"type": "string", "format": "date-time"}, |
475 | + "release": {"type": "string"}, |
476 | + "packages": { |
477 | + "type": "object", |
478 | + "additionalProperties": {"$ref": "#/$defs/package"} |
479 | + }, |
480 | + "security_issues": { |
481 | + "type": "object", |
482 | + "properties": { |
483 | + "cves": { |
484 | + "type": "object", |
485 | + "additionalProperties": {"$ref": "#/$defs/cve"} |
486 | + }, |
487 | + "usns": { |
488 | + "type": "object", |
489 | + "additionalProperties": {"$ref": "#/$defs/usn"} |
490 | + } |
491 | + }, |
492 | + "required": ["cves", "usns"] |
493 | + } |
494 | + }, |
495 | + "$defs": { |
496 | + "package": { |
497 | + "type": "object", |
498 | + "properties": { |
499 | + "source_versions": { |
500 | + "type": "object", |
501 | + "additionalProperties": {"$ref": "#/$defs/sourceVersion"} |
502 | + }, |
503 | + "ubuntu_security_notices": { |
504 | + "type": "object", |
505 | + "additionalProperties": {"$ref": "#/$defs/securityNotice"} |
506 | + }, |
507 | + "ubuntu_security_notices_regressions": { |
508 | + "type": "object", |
509 | + "additionalProperties": {"$ref": "#/$defs/securityNoticeRegression"} |
510 | + }, |
511 | + "cves": { |
512 | + "type": "object", |
513 | + "additionalProperties": {"$ref": "#/$defs/cvePkg"} |
514 | + } |
515 | + }, |
516 | + "required": ["source_versions"] |
517 | + }, |
518 | + "sourceVersion": { |
519 | + "type": "object", |
520 | + "properties": { |
521 | + "binary_packages": { |
522 | + "type": "object", |
523 | + "additionalProperties": {"type": "string"} |
524 | + }, |
525 | + "pocket": {"type": "string"} |
526 | + }, |
527 | + "required": ["binary_packages", "pocket"] |
528 | + }, |
529 | + "securityNotice": { |
530 | + "type": "object", |
531 | + "properties": { |
532 | + "source_fixed_version": {"type": ["string", "null"]} |
533 | + }, |
534 | + "required": ["source_fixed_version"] |
535 | + }, |
536 | + "securityNoticeRegression": { |
537 | + "type": "object", |
538 | + "properties": { |
539 | + "source_fixed_version": {"type": ["string", "null"]} |
540 | + }, |
541 | + "required": ["source_fixed_version"] |
542 | + }, |
543 | + "cvePkg": { |
544 | + "type": "object", |
545 | + "properties": { |
546 | + "source_fixed_version": {"type": ["string", "null"]}, |
547 | + "ubuntu_priority": { |
548 | + "type": "string", |
549 | + "enum": ["untriaged", "negligible", "low", "medium", "high", "critical"] |
550 | + }, |
551 | + "status": { |
552 | + "type": "string", |
553 | + "enum": ["not-vulnerable", "vulnerable", "fixed"] |
554 | + } |
555 | + }, |
556 | + "required": ["source_fixed_version", "ubuntu_priority", "status"] |
557 | + }, |
558 | + "cve": { |
559 | + "type": "object", |
560 | + "properties": { |
561 | + "description": {"type": "string"}, |
562 | + "published_at": {"type": "string", "format": "date-time"}, |
563 | + "notes": { |
564 | + "type": "array", |
565 | + "items": {"type": "string"} |
566 | + }, |
567 | + "mitigation": {"type": ["string", "null"]}, |
568 | + "cvss_severity": {"type": ["string", "null"]}, |
569 | + "cvss_score": {"type": ["number", "null"]} |
570 | + }, |
571 | + "required": ["description", "published_at", "cvss_severity", "cvss_score"] |
572 | + }, |
573 | + "usn": { |
574 | + "type": "object", |
575 | + "properties": { |
576 | + "description": {"type": "string"}, |
577 | + "published_at": {"type": "string", "format": "date-time"}, |
578 | + "related_cves": { |
579 | + "type": "array", |
580 | + "items": {"type": "string"} |
581 | + }, |
582 | + "related_bugs": { |
583 | + "type": "array", |
584 | + "items": {"type": "string"} |
585 | + } |
586 | + }, |
587 | + "required": ["description", "published_at"] |
588 | + } |
589 | + }, |
590 | + "required": ["format", "published_at", "release", "packages", "security_issues"] |
591 | +} |
592 | diff --git a/test/test_json_generation.py b/test/test_json_generation.py |
593 | new file mode 100644 |
594 | index 0000000..a7eec6c |
595 | --- /dev/null |
596 | +++ b/test/test_json_generation.py |
597 | @@ -0,0 +1,837 @@ |
598 | +import pytest |
599 | +from oval_lib import JSONPkgGenerator, Package, CVE, USN |
600 | +from dateutil import parser, tz |
601 | +from datetime import datetime |
602 | + |
603 | +def generate_mock_cve(pkgs, cve_id='CVE-0001-0001', |
604 | + description='test', |
605 | + priority='medium', |
606 | + public_date='2020-08-04 17:00:00 UTC', |
607 | + cvss = [{ |
608 | + 'baseScore': '8.8', |
609 | + 'vector': '3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H', |
610 | + 'baseSeverity': 'HIGH' |
611 | + }], |
612 | + assigned_to='foo', |
613 | + discovered_by='johndoe', |
614 | + notes=[ |
615 | + ('test', 'note'), |
616 | + ('test2', 'note2') |
617 | + ], |
618 | + mitigation='test', |
619 | + references= 'http://example.com\nhttp://example2.com\n', |
620 | + bugs='http://bug1.com\nhttp://bug2.com\n', |
621 | + ): |
622 | + cve = CVE( |
623 | + number=cve_id, |
624 | + info={ |
625 | + 'Description': description, |
626 | + 'Priority': [priority], |
627 | + 'PublicDate': public_date, |
628 | + 'CVSS': cvss, |
629 | + 'Assigned-to': assigned_to, |
630 | + 'Discovered-by': discovered_by, |
631 | + 'Notes': notes, |
632 | + 'Mitigation': mitigation, |
633 | + 'References': references, |
634 | + 'Bugs': bugs |
635 | + } |
636 | + ) |
637 | + |
638 | + for pkg, info in pkgs.items(): |
639 | + cve.add_pkg(pkg, info['release'], info['state'], info['note']) |
640 | + |
641 | + return cve |
642 | + |
643 | +def generate_mock_pkg(version_binaries, |
644 | + pkgname='foo', |
645 | + rel='jammy', |
646 | + component='main', |
647 | + ): |
648 | + pkg = Package(pkgname, rel, version_binaries) |
649 | + pkg.section = component |
650 | + |
651 | + return pkg |
652 | + |
653 | +def generate_mock_usn(description=None, releases=None, title=None, timestamp=None, summary=None, action=None, id=None, isummary=None, cve_objs={}, pkgs_by_rel={}, lp_bugs=[]): |
654 | + default_values = { |
655 | + 'description': 'Default Description', |
656 | + 'releases': ['jammy'], |
657 | + 'title': 'Default Title', |
658 | + 'timestamp': datetime.now().isoformat(timespec="seconds"), |
659 | + 'summary': 'Default Summary', |
660 | + 'action': 'Default Action', |
661 | + 'id': 'UNS-0001-1', |
662 | + 'isummary': 'Default ISummary' |
663 | + } |
664 | + |
665 | + # Update default values with provided values |
666 | + default_values.update({ |
667 | + k: v for k, v in locals().items() if k in default_values and v is not None |
668 | + }) |
669 | + |
670 | + # Create and return instance of MyClass with provided or default values |
671 | + return USN(default_values, cve_objs, pkgs_by_rel, lp_bugs) |
672 | + |
673 | + |
674 | +def generate_version_binaries(source_depth, binaries, different_binary_versions=False, esm=False): |
675 | + """ |
676 | + "0ad": { |
677 | + "0.0.25b-1": { |
678 | + "binaries": { |
679 | + "0ad": { |
680 | + "arch": [ |
681 | + "amd64", |
682 | + "arm64", |
683 | + "armhf" |
684 | + ], |
685 | + "component": "universe", |
686 | + "version": "0.0.25b-1" |
687 | + }, |
688 | + "0ad-dbgsym": { |
689 | + "arch": [ |
690 | + "amd64", |
691 | + "arm64", |
692 | + "armhf" |
693 | + ], |
694 | + "component": "universe", |
695 | + "version": "0.0.25b-1" |
696 | + } |
697 | + }, |
698 | + "component": "universe", |
699 | + "pocket": "Release" |
700 | + }, |
701 | + |
702 | + """ |
703 | + version_binaries = {} |
704 | + for base_source_version in range(source_depth): |
705 | + source_version = str(base_source_version) |
706 | + if esm: |
707 | + source_version = source_version + '+' + 'esm1' |
708 | + |
709 | + version_binaries[source_version] = {} |
710 | + for binary_version in range(len(binaries)): |
711 | + binary_version_modifier = 1 |
712 | + version = str(binary_version) |
713 | + if different_binary_versions: |
714 | + version = version + '+' + str(binary_version_modifier) |
715 | + binary_version_modifier += 1 |
716 | + if esm: |
717 | + version = version + '+' + 'esm1' |
718 | + version_binaries[source_version][version] = [] |
719 | + version_binaries[source_version][version].append(binaries[binary_version]) |
720 | + |
721 | + return version_binaries |
722 | + |
723 | + |
724 | +class EmptyJSONPkgGenerator(JSONPkgGenerator): |
725 | + def __init__(self, releases = ['jammy']) -> None: |
726 | + self.releases = releases |
727 | + self.pkg_cache = None |
728 | + self.expand = True |
729 | + pass |
730 | + |
731 | +#### TESTS #### |
732 | + |
733 | +@pytest.mark.parametrize("mock_cve", [ |
734 | + generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001'), |
735 | + generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', public_date='2020-08-04 17:00:00 PDT'), |
736 | + generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', public_date='2020-08-04 17:00:00 PST'), |
737 | + generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0004', cvss=None) |
738 | +]) |
739 | +def test_generate_cve_info(mock_cve): |
740 | + json_gen = EmptyJSONPkgGenerator() |
741 | + generated_info = json_gen._generate_cve_info(mock_cve) |
742 | + assert generated_info['description'] == mock_cve.description.strip() |
743 | + assert generated_info['published_at'] == parser.parse(mock_cve.public_date, |
744 | + tzinfos={'PDT': tz.gettz('America/Los_Angeles'), |
745 | + 'PST': tz.gettz('America/Los_Angeles') |
746 | + } |
747 | + ).isoformat(timespec="seconds") |
748 | + |
749 | + notes = [] |
750 | + for note in mock_cve.notes: |
751 | + notes.append(f"{note[0]}> {note[1]}") |
752 | + |
753 | + assert generated_info['notes'] == notes |
754 | + assert generated_info['mitigation'] == mock_cve.mitigation |
755 | + assert generated_info['cvss_severity'] == (mock_cve.cvss[0]['baseSeverity'].lower() if mock_cve.cvss else None) |
756 | + assert generated_info['cvss_score'] == (float(mock_cve.cvss[0]['baseScore']) if mock_cve.cvss else None) |
757 | + |
758 | +def test_generate_cves_info(): |
759 | + cves = { |
760 | + 'CVE-0001-0001': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001'), |
761 | + 'CVE-0001-0002': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', public_date='2020-08-04 17:00:00 PDT'), |
762 | + 'CVE-0001-0003': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', public_date='2020-08-04 17:00:00 PST'), |
763 | + 'CVE-0001-0004': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0004', cvss=None) |
764 | + } |
765 | + |
766 | + json_gen = EmptyJSONPkgGenerator() |
767 | + json_gen.cves = {'jammy': {}} |
768 | + |
769 | + for cve_id, cve in cves.items(): |
770 | + json_gen.cves[cve_id] = cve |
771 | + |
772 | + json_gen._init_ids('jammy') |
773 | + generated_info_cves = json_gen._generate_cves_info() |
774 | + |
775 | + for cve_id, generated_info in generated_info_cves.items(): |
776 | + mock_cve = cves[cve_id] |
777 | + assert generated_info['description'] == mock_cve.description.strip() |
778 | + assert generated_info['published_at'] == parser.parse(mock_cve.public_date, |
779 | + tzinfos={'PDT': tz.gettz('America/Los_Angeles'), |
780 | + 'PST': tz.gettz('America/Los_Angeles') |
781 | + } |
782 | + ).isoformat(timespec="seconds") |
783 | + assert generated_info['notes'] == mock_cve.notes |
784 | + assert generated_info['mitigation'] == mock_cve.mitigation |
785 | + assert generated_info['cvss_severity'] == (mock_cve.cvss[0]['baseSeverity'].lower() if mock_cve.cvss else None) |
786 | + assert generated_info['cvss_score'] == (float |
787 | + (mock_cve.cvss[0]['baseScore']) if mock_cve.cvss else None) |
788 | + |
789 | +def test_generate_cves_info_parents(): |
790 | + cves_jammy = { |
791 | + 'CVE-0001-0001': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001'), |
792 | + 'CVE-0001-0002': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', public_date='2020-08-04 17:00:00 PDT'), |
793 | + } |
794 | + |
795 | + cves_jammy_esm_apps = { |
796 | + 'CVE-0001-0003': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', public_date='2020-08-04 17:00:00 PST'), |
797 | + } |
798 | + |
799 | + cves_jammy_esm_infra = { |
800 | + 'CVE-0001-0004': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0004', cvss=None) |
801 | + } |
802 | + |
803 | + json_gen = EmptyJSONPkgGenerator() |
804 | + json_gen.cves = {'jammy': {}, 'esm-apps/jammy': {}, 'esm-infra/jammy': {}} |
805 | + |
806 | + for cve_id, cve in cves_jammy.items(): |
807 | + json_gen.cves['jammy'][cve_id] = cve |
808 | + |
809 | + for cve_id, cve in cves_jammy_esm_apps.items(): |
810 | + json_gen.cves['esm-apps/jammy'][cve_id] = cve |
811 | + |
812 | + for cve_id, cve in cves_jammy_esm_infra.items(): |
813 | + json_gen.cves['esm-infra/jammy'][cve_id] = cve |
814 | + |
815 | + |
816 | + json_gen._init_ids('jammy') |
817 | + generated_info_cves = json_gen._generate_cves_info() |
818 | + assert len(generated_info_cves) == 2 |
819 | + for cve_id in cves_jammy.keys(): |
820 | + assert cve_id in generated_info_cves.keys() |
821 | + |
822 | + json_gen._init_ids('esm-infra/jammy') |
823 | + json_gen.parent_releases = ['jammy'] |
824 | + generated_info_cves = json_gen._generate_cves_info() |
825 | + assert len(generated_info_cves) == 3 |
826 | + for cve_id in list(cves_jammy.keys()) + list(cves_jammy_esm_infra.keys()): |
827 | + assert cve_id in generated_info_cves.keys() |
828 | + |
829 | + |
830 | + json_gen._init_ids('esm-apps/jammy') |
831 | + json_gen.parent_releases = ['esm-infra/jammy', 'jammy'] |
832 | + generated_info_cves = json_gen._generate_cves_info() |
833 | + assert len(generated_info_cves) == 4 |
834 | + for cve_id in list(cves_jammy.keys()) + list(cves_jammy_esm_infra.keys()) + list(cves_jammy_esm_apps.keys()): |
835 | + assert cve_id in generated_info_cves.keys() |
836 | + |
837 | + |
838 | +@pytest.mark.parametrize("status,note,json_status,fixed_version", [ |
839 | + ("ignored", "not for us", "vulnerable", None), |
840 | + ("needed", "", "vulnerable", None), |
841 | + ("needs-triage", "", "vulnerable", None), |
842 | + ("deferred", "1-2-3", "vulnerable", None), |
843 | + ("not-affected", "code not present", "not-vulnerable", None), |
844 | + ("not-affected", "1.2.3", "fixed", "1.2.3"), |
845 | + ("released", "1.2.3", "fixed", "1.2.3"), |
846 | +]) |
847 | +def test_generate_cve_pkg_info(status, note, json_status, fixed_version): |
848 | + package = generate_mock_pkg(generate_version_binaries( |
849 | + 2, ['bar', 'foo'] |
850 | + )) |
851 | + |
852 | + cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical') |
853 | + cve.add_pkg(package, 'jammy', status, note) |
854 | + json_gen = EmptyJSONPkgGenerator() |
855 | + |
856 | + info = json_gen._generate_cve_pkg_info(package) |
857 | + info = info['CVE-0001-0001'] |
858 | + |
859 | + assert info['source_fixed_version'] == fixed_version |
860 | + assert info['ubuntu_priority'] == 'critical' |
861 | + assert info['status'] == json_status |
862 | + |
863 | +@pytest.mark.parametrize("usn", [ |
864 | + generate_mock_usn( |
865 | + description='this is a test description', |
866 | + timestamp=1708590783, |
867 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
868 | + lp_bugs=['http://bug1.com', 'http://bug2.com'] |
869 | + ), |
870 | + generate_mock_usn( |
871 | + description='this is a test description', |
872 | + timestamp=1708590783, |
873 | + lp_bugs=['http://bug1.com', 'http://bug2.com'] |
874 | + ), |
875 | + generate_mock_usn( |
876 | + description='this is a test description', |
877 | + timestamp=1708590783, |
878 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None} |
879 | + ), |
880 | +]) |
881 | +def test_generate_usn_info(usn): |
882 | + # 1708590783 is ~2024-02-22T09:33:03+00:00 |
883 | + date = datetime.fromtimestamp(1708590783).isoformat(timespec="seconds") |
884 | + json_gen = EmptyJSONPkgGenerator() |
885 | + |
886 | + info = json_gen._generate_usn_info(usn) |
887 | + assert info['description'] == usn.description |
888 | + assert info['published_at'] == date |
889 | + assert info['related_cves'] == list(usn.cves.keys()) |
890 | + assert info['related_launchpad_bugs'] == usn.lp_bugs |
891 | + |
892 | +def test_generate_usns_info(): |
893 | + date = datetime.fromtimestamp(1708590783).isoformat(timespec="seconds") |
894 | + |
895 | + usns = [generate_mock_usn( |
896 | + id='1000-1', |
897 | + description='this is a test description', |
898 | + timestamp=1708590783, |
899 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
900 | + lp_bugs=['http://bug1.com', 'http://bug2.com'] |
901 | + ), |
902 | + generate_mock_usn( |
903 | + id='1000-2', |
904 | + description='this is a test description', |
905 | + timestamp=1708590783, |
906 | + lp_bugs=['http://bug1.com', 'http://bug2.com'] |
907 | + ), |
908 | + generate_mock_usn( |
909 | + id='1000-3', |
910 | + description='this is a test description', |
911 | + timestamp=1708590783, |
912 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None} |
913 | + )] |
914 | + |
915 | + wrong_release_usn = generate_mock_usn( |
916 | + id='2000-1', |
917 | + description='this is a test description', |
918 | + timestamp=1708590783, |
919 | + releases=['bionic'], |
920 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None} |
921 | + ) |
922 | + |
923 | + final_usns = usns.copy() |
924 | + final_usns.append(wrong_release_usn) |
925 | + usns_dict = dict(map(lambda usn: (usn.id, usn), final_usns)) |
926 | + json_gen = EmptyJSONPkgGenerator() |
927 | + json_gen.usns = usns_dict |
928 | + json_gen._init_ids('jammy') |
929 | + info = json_gen._generate_usns_info() |
930 | + |
931 | + assert 'USN-2000-1' not in info |
932 | + |
933 | + for usn in usns: |
934 | + assert f'USN-{usn.id}' in info |
935 | + assert info[f'USN-{usn.id}']['description'] == usn.description |
936 | + assert info[f'USN-{usn.id}']['published_at'] == date |
937 | + assert info[f'USN-{usn.id}']['related_cves'] == list(usn.cves.keys()) |
938 | + assert info[f'USN-{usn.id}']['related_launchpad_bugs'] == usn.lp_bugs |
939 | + |
940 | +def test_generate_usns_info_parents(): |
941 | + jammy_usns = [generate_mock_usn( |
942 | + id='1000-1', |
943 | + description='this is a test description', |
944 | + timestamp=1708590783, |
945 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
946 | + lp_bugs=['http://bug1.com', 'http://bug2.com'] |
947 | + ), |
948 | + generate_mock_usn( |
949 | + id='1000-2', |
950 | + description='this is a test description', |
951 | + timestamp=1708590783, |
952 | + lp_bugs=['http://bug1.com', 'http://bug2.com'] |
953 | + ), |
954 | + generate_mock_usn( |
955 | + id='1000-3', |
956 | + description='this is a test description', |
957 | + timestamp=1708590783, |
958 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None} |
959 | + )] |
960 | + |
961 | + jammy_infra_usns = [ |
962 | + generate_mock_usn( |
963 | + id='2000-1', |
964 | + description='this is a test description', |
965 | + timestamp=1708590783, |
966 | + releases=['esm-infra/jammy'], |
967 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None} |
968 | + ) |
969 | + ] |
970 | + |
971 | + jammy_apps_usns = [ |
972 | + generate_mock_usn( |
973 | + id='3000-1', |
974 | + description='this is a test description', |
975 | + timestamp=1708590783, |
976 | + releases=['esm-apps/jammy'], |
977 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None} |
978 | + ) |
979 | + ] |
980 | + |
981 | + final_usns = jammy_usns + jammy_infra_usns + jammy_apps_usns |
982 | + usns_dict = dict(map(lambda usn: (usn.id, usn), final_usns)) |
983 | + json_gen = EmptyJSONPkgGenerator() |
984 | + json_gen.usns = usns_dict |
985 | + json_gen._init_ids('jammy') |
986 | + info = json_gen._generate_usns_info() |
987 | + assert len(info.keys()) == 3 |
988 | + for usn in jammy_usns: |
989 | + assert f'USN-{usn.id}' in info |
990 | + |
991 | + json_gen._init_ids('esm-infra/jammy') |
992 | + json_gen.parent_releases = ['jammy'] |
993 | + info = json_gen._generate_usns_info() |
994 | + assert len(info.keys()) == 4 |
995 | + for usn in jammy_usns + jammy_infra_usns: |
996 | + assert f'USN-{usn.id}' in info |
997 | + |
998 | + json_gen._init_ids('esm-apps/jammy') |
999 | + json_gen.parent_releases = ['esm-infra/jammy', 'jammy'] |
1000 | + info = json_gen._generate_usns_info() |
1001 | + assert len(info.keys()) == 5 |
1002 | + for usn in jammy_usns + jammy_infra_usns + jammy_apps_usns: |
1003 | + assert f'USN-{usn.id}' in info |
1004 | + |
1005 | +def test_generate_usn_pkg_info(): |
1006 | + releases_1 = { |
1007 | + 'jammy': { |
1008 | + 'sources': { |
1009 | + 'foo': { |
1010 | + 'version': '1.0.0' |
1011 | + } |
1012 | + } |
1013 | + } |
1014 | + } |
1015 | + |
1016 | + releases_2 = { |
1017 | + 'jammy': { |
1018 | + 'sources': { |
1019 | + 'foo': { |
1020 | + 'version': '2.0.0' |
1021 | + } |
1022 | + } |
1023 | + } |
1024 | + } |
1025 | + |
1026 | + pkg = generate_mock_pkg(generate_version_binaries( |
1027 | + 1, ['bar', 'foo'] |
1028 | + )) |
1029 | + |
1030 | + pkg_not = generate_mock_pkg(generate_version_binaries( |
1031 | + 1, ['bar', 'foo'] |
1032 | + ), rel='bionic') |
1033 | + |
1034 | + usn = generate_mock_usn( |
1035 | + id='1000-1', |
1036 | + description='this is a test description', |
1037 | + timestamp=1708590783, |
1038 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1039 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1040 | + pkgs_by_rel={'jammy': {'foo': pkg}}, |
1041 | + releases=releases_1 |
1042 | + ) |
1043 | + |
1044 | + usn_regression = generate_mock_usn( |
1045 | + id='1000-2', |
1046 | + title='Regression', |
1047 | + description='this is a test description', |
1048 | + timestamp=1708590783, |
1049 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1050 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1051 | + pkgs_by_rel={'jammy': {'foo': pkg}}, |
1052 | + releases=releases_2 |
1053 | + ) |
1054 | + |
1055 | + json_gen = EmptyJSONPkgGenerator() |
1056 | + usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_regression])) |
1057 | + json_gen.usns = usns_dict |
1058 | + |
1059 | + usn_info, usn_reg_info = json_gen._generate_usn_pkg_info(pkg) |
1060 | + |
1061 | + assert 'USN-1000-1' in usn_info |
1062 | + assert 'USN-1000-2' in usn_reg_info |
1063 | + assert 'USN-1000-1' not in usn_reg_info |
1064 | + assert 'USN-1000-2' not in usn_info |
1065 | + |
1066 | + assert usn_info['USN-1000-1']['source_fixed_version'] == '1.0.0' |
1067 | + assert usn_reg_info['USN-1000-2']['source_fixed_version'] == '2.0.0' |
1068 | + |
1069 | + usn_info, usn_reg_info = json_gen._generate_usn_pkg_info(pkg_not) |
1070 | + |
1071 | + assert 'USN-1000-1' not in usn_info |
1072 | + assert 'USN-1000-2' not in usn_reg_info |
1073 | + assert 'USN-1000-1' not in usn_reg_info |
1074 | + assert 'USN-1000-2' not in usn_info |
1075 | + |
1076 | + |
1077 | +@pytest.mark.parametrize("package,pocket", [ |
1078 | + (generate_mock_pkg(generate_version_binaries( |
1079 | + 1, ['bar', 'foo'] |
1080 | + )), "security"), |
1081 | + (generate_mock_pkg(generate_version_binaries( |
1082 | + 2, ['bar', 'foo'], different_binary_versions=True |
1083 | + )), "release"), |
1084 | + (generate_mock_pkg(generate_version_binaries( |
1085 | + 3, ['bar', 'foo'], esm=True, |
1086 | + )), "esm-infra"), |
1087 | + (generate_mock_pkg(generate_version_binaries( |
1088 | + 4, ['bar', 'foo'], esm=True |
1089 | + ), component='universe'), "esm-apps"), |
1090 | +]) |
1091 | +def test_generate_package_source_info(mocker, package, pocket): |
1092 | + mocker.patch('oval_lib.get_pocket', return_value=('',pocket)) |
1093 | + json_gen = EmptyJSONPkgGenerator() |
1094 | + info = dict() |
1095 | + json_gen._generate_package_source_info(package, info) |
1096 | + |
1097 | + for source_version in package.versions_binaries: |
1098 | + assert source_version in info |
1099 | + for binary_version in package.versions_binaries[source_version]: |
1100 | + for binary in package.versions_binaries[source_version][binary_version]: |
1101 | + assert binary in info[source_version]['binary_packages'] |
1102 | + assert info[source_version]['binary_packages'][binary] == binary_version |
1103 | + |
1104 | + assert info[source_version]['pocket'] == pocket |
1105 | + |
1106 | +def test_generate_package_info(mocker): |
1107 | + mocker.patch('oval_lib.get_pocket', return_value=('','release')) |
1108 | + |
1109 | + releases_1 = { |
1110 | + 'jammy': { |
1111 | + 'sources': { |
1112 | + 'foo': { |
1113 | + 'version': '1.0.0' |
1114 | + } |
1115 | + } |
1116 | + } |
1117 | + } |
1118 | + |
1119 | + releases_2 = { |
1120 | + 'bionic': { |
1121 | + 'sources': { |
1122 | + 'bar': { |
1123 | + 'version': '2.0.0' |
1124 | + } |
1125 | + } |
1126 | + } |
1127 | + } |
1128 | + |
1129 | + pkg = generate_mock_pkg(generate_version_binaries( |
1130 | + 1, ['bar', 'foo'] |
1131 | + )) |
1132 | + |
1133 | + pkg2 = generate_mock_pkg(generate_version_binaries( |
1134 | + 2, ['bar', 'foo'] |
1135 | + ), rel='bionic', pkgname='bar') |
1136 | + |
1137 | + usn = generate_mock_usn( |
1138 | + id='1000-1', |
1139 | + description='this is a test description', |
1140 | + timestamp=1708590783, |
1141 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1142 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1143 | + pkgs_by_rel={'jammy': {'foo': pkg}}, |
1144 | + releases=releases_1 |
1145 | + ) |
1146 | + |
1147 | + usn_regression = generate_mock_usn( |
1148 | + id='1000-2', |
1149 | + title='Regression', |
1150 | + description='this is a test description', |
1151 | + timestamp=1708590783, |
1152 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1153 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1154 | + pkgs_by_rel={'bionic': {'bar': pkg2}}, |
1155 | + releases=releases_2 |
1156 | + ) |
1157 | + |
1158 | + |
1159 | + cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical') |
1160 | + cve2 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', priority='medium') |
1161 | + |
1162 | + cve.add_pkg(pkg, 'jammy', 'needed', '') |
1163 | + cve2.add_pkg(pkg2, 'bionic', 'not-affected', '1') |
1164 | + json_gen = EmptyJSONPkgGenerator() |
1165 | + usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_regression])) |
1166 | + json_gen.usns = usns_dict |
1167 | + json_gen.cves = dict() |
1168 | + json_gen.cves['CVE-0001-0001'] = cve |
1169 | + json_gen.cves['CVE-0001-0002'] = cve2 |
1170 | + |
1171 | + json_gen.packages = {} |
1172 | + json_gen.packages['jammy'] = { |
1173 | + 'foo': pkg |
1174 | + } |
1175 | + |
1176 | + json_gen.packages['bionic'] = { |
1177 | + 'bar': pkg2 |
1178 | + } |
1179 | + |
1180 | + json_gen._init_ids('jammy') |
1181 | + info = json_gen._generate_package_info(pkg) |
1182 | + assert '0' in info['source_versions'] |
1183 | + assert 'USN-1000-1' in info['ubuntu_security_notices'] |
1184 | + assert len(info['ubuntu_security_notices_regressions']) == 0 |
1185 | + assert 'CVE-0001-0001' in info['cves'] |
1186 | + assert 'CVE-0001-0002' not in info['cves'] |
1187 | + |
1188 | + json_gen._init_ids('bionic') |
1189 | + info = json_gen._generate_package_info(pkg2) |
1190 | + assert '0' in info['source_versions'] |
1191 | + assert 'USN-1000-2' in info['ubuntu_security_notices_regressions'] |
1192 | + assert len(info['ubuntu_security_notices']) == 0 |
1193 | + assert 'CVE-0001-0002' in info['cves'] |
1194 | + assert 'CVE-0001-0001' not in info['cves'] |
1195 | + |
1196 | +def test_generate_packages_info(mocker): |
1197 | + mocker.patch('oval_lib.get_pocket', return_value=('','release')) |
1198 | + |
1199 | + releases_1 = { |
1200 | + 'jammy': { |
1201 | + 'sources': { |
1202 | + 'foo': { |
1203 | + 'version': '1.0.0' |
1204 | + } |
1205 | + } |
1206 | + } |
1207 | + } |
1208 | + |
1209 | + releases_2 = { |
1210 | + 'bionic': { |
1211 | + 'sources': { |
1212 | + 'bar': { |
1213 | + 'version': '2.0.0' |
1214 | + } |
1215 | + } |
1216 | + } |
1217 | + } |
1218 | + |
1219 | + pkg = generate_mock_pkg(generate_version_binaries( |
1220 | + 1, ['bar', 'foo'] |
1221 | + )) |
1222 | + |
1223 | + pkg2 = generate_mock_pkg(generate_version_binaries( |
1224 | + 2, ['bar', 'foo'] |
1225 | + ), rel='bionic', pkgname='bar') |
1226 | + |
1227 | + usn = generate_mock_usn( |
1228 | + id='1000-1', |
1229 | + description='this is a test description', |
1230 | + timestamp=1708590783, |
1231 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1232 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1233 | + pkgs_by_rel={'jammy': {'foo': pkg}}, |
1234 | + releases=releases_1 |
1235 | + ) |
1236 | + |
1237 | + usn_regression = generate_mock_usn( |
1238 | + id='1000-2', |
1239 | + title='Regression', |
1240 | + description='this is a test description', |
1241 | + timestamp=1708590783, |
1242 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1243 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1244 | + pkgs_by_rel={'bionic': {'bar': pkg2}}, |
1245 | + releases=releases_2 |
1246 | + ) |
1247 | + |
1248 | + |
1249 | + cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical') |
1250 | + cve2 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', priority='medium') |
1251 | + |
1252 | + cve.add_pkg(pkg, 'jammy', 'needed', '') |
1253 | + cve2.add_pkg(pkg2, 'bionic', 'not-affected', '1') |
1254 | + json_gen = EmptyJSONPkgGenerator() |
1255 | + usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_regression])) |
1256 | + json_gen.usns = usns_dict |
1257 | + json_gen.cves = dict() |
1258 | + json_gen.cves['CVE-0001-0001'] = cve |
1259 | + json_gen.cves['CVE-0001-0002'] = cve2 |
1260 | + json_gen.packages = { |
1261 | + 'jammy': { |
1262 | + 'foo': pkg |
1263 | + }, |
1264 | + 'bionic': { |
1265 | + 'bar': pkg2 |
1266 | + } |
1267 | + } |
1268 | + |
1269 | + json_gen._init_ids('jammy') |
1270 | + info = json_gen._generate_packages_info() |
1271 | + assert 'foo' in info |
1272 | + assert 'bar' not in info |
1273 | + |
1274 | + json_gen._init_ids('bionic') |
1275 | + info = json_gen._generate_packages_info() |
1276 | + assert 'foo' not in info |
1277 | + assert 'bar' in info |
1278 | + |
1279 | +def test_generate_packages_info_parents(mocker): |
1280 | + mocker.patch('oval_lib.get_pocket', return_value=('','release')) |
1281 | + |
1282 | + releases_jammy = { |
1283 | + 'jammy': { |
1284 | + 'sources': { |
1285 | + 'foo': { |
1286 | + 'version': '1.0.0' |
1287 | + } |
1288 | + } |
1289 | + } |
1290 | + } |
1291 | + |
1292 | + releases_infra_jammy = { |
1293 | + 'esm-infra/jammy': { |
1294 | + 'sources': { |
1295 | + 'bar': { |
1296 | + 'version': '2.0.0' |
1297 | + } |
1298 | + } |
1299 | + } |
1300 | + } |
1301 | + |
1302 | + releases_apps_jammy = { |
1303 | + 'esm-apps/jammy': { |
1304 | + 'sources': { |
1305 | + 'dodo': { |
1306 | + 'version': '3.0.0' |
1307 | + } |
1308 | + } |
1309 | + } |
1310 | + } |
1311 | + |
1312 | + pkg = generate_mock_pkg(generate_version_binaries( |
1313 | + 1, ['bar', 'foo'] |
1314 | + )) |
1315 | + |
1316 | + pkg2 = generate_mock_pkg(generate_version_binaries( |
1317 | + 2, ['bar', 'fofa'] |
1318 | + ), rel='esm-infra/jammy', pkgname='bar') |
1319 | + |
1320 | + |
1321 | + pkg3 = generate_mock_pkg(generate_version_binaries( |
1322 | + 2, ['dodo', 'foo'] |
1323 | + ), rel='esm-apps/jammy', pkgname='dodo') |
1324 | + |
1325 | + pkg4 = generate_mock_pkg(generate_version_binaries( |
1326 | + 3, ['bar', 'foo'] |
1327 | + ), rel='esm-apps/jammy') |
1328 | + |
1329 | + |
1330 | + usn = generate_mock_usn( |
1331 | + id='1000-1', |
1332 | + description='this is a test description', |
1333 | + timestamp=1708590783, |
1334 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1335 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1336 | + pkgs_by_rel={'jammy': {'foo': pkg}}, |
1337 | + releases=releases_jammy |
1338 | + ) |
1339 | + |
1340 | + usn_infra = generate_mock_usn( |
1341 | + id='2000-1', |
1342 | + title='Infra', |
1343 | + description='this is a test description', |
1344 | + timestamp=1708590783, |
1345 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1346 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1347 | + pkgs_by_rel={'esm-infra/jammy': {'bar': pkg2}}, |
1348 | + releases=releases_infra_jammy |
1349 | + ) |
1350 | + |
1351 | + usn_apps = generate_mock_usn( |
1352 | + id='3000-1', |
1353 | + title='Apps', |
1354 | + description='this is a test description', |
1355 | + timestamp=1708590783, |
1356 | + cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}, |
1357 | + lp_bugs=['http://bug1.com', 'http://bug2.com'], |
1358 | + pkgs_by_rel={'esm-apps/jammy': {'dodo': pkg3}}, |
1359 | + releases=releases_apps_jammy |
1360 | + ) |
1361 | + |
1362 | + cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical') |
1363 | + cve2 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', priority='medium') |
1364 | + cve3 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', priority='medium') |
1365 | + |
1366 | + cve.add_pkg(pkg, 'jammy', 'needed', '') |
1367 | + cve.add_pkg(pkg4, 'esm-apps/jammy', 'not-affected', '4') |
1368 | + cve2.add_pkg(pkg2, 'esm-infra/jammy', 'not-affected', '1') |
1369 | + cve3.add_pkg(pkg3, 'esm-apps/jammy', 'released', '2') |
1370 | + |
1371 | + json_gen = EmptyJSONPkgGenerator() |
1372 | + usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_infra, usn_apps])) |
1373 | + json_gen.usns = usns_dict |
1374 | + json_gen.cves = {'jammy': {}, 'esm-apps/jammy': {}, 'esm-infra/jammy': {}} |
1375 | + json_gen.cves['jammy']['CVE-0001-0001'] = cve |
1376 | + json_gen.cves['esm-infra/jammy']['CVE-0001-0002'] = cve2 |
1377 | + json_gen.cves['esm-apps/jammy']['CVE-0001-0003'] = cve3 |
1378 | + |
1379 | + json_gen.packages = { |
1380 | + 'jammy': { |
1381 | + 'foo': pkg |
1382 | + }, |
1383 | + 'esm-infra/jammy': { |
1384 | + 'bar': pkg2 |
1385 | + }, |
1386 | + 'esm-apps/jammy': { |
1387 | + 'dodo': pkg3, |
1388 | + 'foo': pkg4 |
1389 | + } |
1390 | + } |
1391 | + |
1392 | + json_gen._init_ids('jammy') |
1393 | + info = json_gen._generate_packages_info() |
1394 | + assert 'foo' in info |
1395 | + assert 'bar' not in info |
1396 | + assert 'dodo' not in info |
1397 | + assert info['foo']['cves']['CVE-0001-0001']['status'] == 'vulnerable' |
1398 | + |
1399 | + json_gen._init_ids('esm-infra/jammy') |
1400 | + json_gen.parent_releases = ['jammy'] |
1401 | + info = json_gen._generate_packages_info() |
1402 | + assert 'foo' in info |
1403 | + assert 'bar' in info |
1404 | + assert 'dodo' not in info |
1405 | + assert info['foo']['cves']['CVE-0001-0001']['status'] == 'vulnerable' |
1406 | + assert info['bar']['cves']['CVE-0001-0002']['status'] == 'fixed' |
1407 | + assert info['bar']['ubuntu_security_notices']['USN-2000-1']['source_fixed_version'] == '2.0.0' |
1408 | + |
1409 | + json_gen._init_ids('esm-apps/jammy') |
1410 | + json_gen.parent_releases = ['esm-infra/jammy', 'jammy'] |
1411 | + info = json_gen._generate_packages_info() |
1412 | + assert 'foo' in info |
1413 | + assert 'bar' in info |
1414 | + assert 'dodo' in info |
1415 | + assert info['foo']['cves']['CVE-0001-0001']['status'] == 'fixed' |
1416 | + assert info['bar']['cves']['CVE-0001-0002']['status'] == 'fixed' |
1417 | + assert info['dodo']['cves']['CVE-0001-0003']['status'] == 'fixed' |
1418 | + assert info['foo']['cves']['CVE-0001-0001']['source_fixed_version'] == '4' |
1419 | + assert info['dodo']['ubuntu_security_notices']['USN-3000-1']['source_fixed_version'] == '3.0.0' |
1420 | + |
1421 | +@pytest.mark.parametrize("expand,release,filename", [ |
1422 | + (False, 'esm-apps/bionic', 'com.ubuntu.bionic.pkg.json'), |
1423 | + (True, 'esm-apps/bionic', 'com.ubuntu.esm-apps_bionic.pkg.json'), |
1424 | + (False, 'esm-infra/bionic', 'com.ubuntu.bionic.pkg.json'), |
1425 | + (True, 'esm-infra/bionic', 'com.ubuntu.esm-infra_bionic.pkg.json'), |
1426 | + (False, 'bionic', 'com.ubuntu.bionic.pkg.json'), |
1427 | + (True, 'bionic', 'com.ubuntu.bionic.pkg.json') |
1428 | +]) |
1429 | +def test_expand(expand, release, filename): |
1430 | + json_gen = EmptyJSONPkgGenerator() |
1431 | + json_gen.expand = expand |
1432 | + json_gen._init_ids(release) |
1433 | + |
1434 | + assert json_gen.output_file == filename |
It also modifies the following OVAL behavior:
* Package won't fail if sources are not provided when loading. releases added to the main OVAL generator since it was needed for collecting the needed sources in the JSON generator. This refactors the code embedded in _load, it doesn't add new code.
* USNs now support lp_bugs.
* Function _get_parent_