Merge ~litios/ubuntu-cve-tracker:json-pkg-gen into ubuntu-cve-tracker:master

Proposed by David Fernandez Gonzalez
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)
Reviewer Review Type Date Requested Status
Eduardo Barretto Approve
Review via email: mp+461645@code.launchpad.net

Description of the change

This is the JSON Generator as per https://warthogs.atlassian.net/browse/SEC-2930.

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.RELEASE.pkg.json'.

Unit testing has been added to test/test_json_generation.py.

To post a comment you must log in.
Revision history for this message
David Fernandez Gonzalez (litios) wrote :

It also modifies the following OVAL behavior:

* Package won't fail if sources are not provided when loading.
* USNs now support lp_bugs.
* Function _get_parent_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.

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>

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

a few things to fix, nothing major

review: Needs Fixing
670af96... by David Fernandez Gonzalez

[JSON] Styling issues

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

Revision history for this message
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>

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.launchpad.yaml b/.launchpad.yaml
2index 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
23diff --git a/scripts/generate-oval b/scripts/generate-oval
24index 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()
88diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py
89index 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
463diff --git a/test/json-gen-schema.json b/test/json-gen-schema.json
464new file mode 100644
465index 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+}
592diff --git a/test/test_json_generation.py b/test/test_json_generation.py
593new file mode 100644
594index 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

Subscribers

People subscribed via source and target branches