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
diff --git a/.launchpad.yaml b/.launchpad.yaml
index 9bcdb5d..e672a85 100644
--- a/.launchpad.yaml
+++ b/.launchpad.yaml
@@ -26,6 +26,8 @@ jobs:
26 - python3-pytest-cov26 - python3-pytest-cov
27 - python3-yaml27 - python3-yaml
28 - shellcheck28 - shellcheck
29 - python3-dateutil
30 - python3-pytest-mock
29 run-before: |31 run-before: |
30 # configure a basic ~/.ubuntu-cve-tracker.conf32 # configure a basic ~/.ubuntu-cve-tracker.conf
31 echo plb_authentication=/dev/null > ~/.ubuntu-cve-tracker.conf33 echo plb_authentication=/dev/null > ~/.ubuntu-cve-tracker.conf
@@ -42,7 +44,7 @@ jobs:
42 echo "reporting syntax issues on scripts"44 echo "reporting syntax issues on scripts"
43 make check-syntax-scripts45 make check-syntax-scripts
44 echo "Running unit tests..."46 echo "Running unit tests..."
45 pytest-3 --cov=scripts ./scripts/test_*.py ./test/test_oval_lib_unit.py47 pytest-3 --cov=scripts ./scripts/test_*.py ./test/test_oval_lib_unit.py ./test/test_json_generation.py
46 check-cves:48 check-cves:
47 series: jammy49 series: jammy
48 architectures: amd6450 architectures: amd64
diff --git a/scripts/generate-oval b/scripts/generate-oval
index cc44ee7..d01329f 100755
--- a/scripts/generate-oval
+++ b/scripts/generate-oval
@@ -45,7 +45,7 @@ def main():
4545
46 # parse command line options46 # parse command line options
47 parser = argparse.ArgumentParser(description='Generate OVAL for CVE, PKG or USNs.')47 parser = argparse.ArgumentParser(description='Generate OVAL for CVE, PKG or USNs.')
48 parser.add_argument('--type', required=True, choices=['cve', 'pkg', 'usn'],48 parser.add_argument('--type', required=True, choices=['cve', 'pkg', 'usn', 'json-pkg'],
49 help='OVAL format')49 help='OVAL format')
50 parser.add_argument('--cves', nargs='*', default=['active/CVE-*', 'retired/CVE-*'],50 parser.add_argument('--cves', nargs='*', default=['active/CVE-*', 'retired/CVE-*'],
51 help='pathname patterns (globs) specifying CVE '51 help='pathname patterns (globs) specifying CVE '
@@ -104,7 +104,7 @@ def main():
104 for r in set(all_releases).difference(set(eol_releases)).difference(set([devel_release])):104 for r in set(all_releases).difference(set(eol_releases)).difference(set([devel_release])):
105 if needs_oval(r):105 if needs_oval(r):
106 releases.append(r)106 releases.append(r)
107 107
108 out_releases = releases108 out_releases = releases
109109
110 # for each release we need to get its parent to also110 # for each release we need to get its parent to also
@@ -124,8 +124,9 @@ def main():
124 out_releases = set(out_releases) - parent_releases124 out_releases = set(out_releases) - parent_releases
125125
126 if args.type == 'usn':126 if args.type == 'usn':
127 generate_oval_usn(args.output_dir, args.usn_number, releases,127 generate_oval_usn(args.output_dir, args.usn_number, releases,
128 args.cves, args.usn_db_dir, args.no_progress, args.oci_prefix, args.oci_output_dir)128 args.cves, args.usn_db_dir, args.no_progress,
129 args.oci_prefix, args.oci_output_dir)
129 else:130 else:
130 cache = {}131 cache = {}
131 for release in releases:132 for release in releases:
@@ -136,6 +137,12 @@ def main():
136 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)137 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)
137 elif args.type == 'cve':138 elif args.type == 'cve':
138 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)139 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)
140 elif args.type == 'json-pkg':
141 usn_database = get_usn_database(args.usn_db_dir)
142 if not usn_database:
143 error("Error getting local USN database. Please, run '$UCT/scripts/fetch-db database.json.bz2' to retrieve the database and try again.")
144
145 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)
139146
140147
141def warn(message):148def warn(message):
@@ -292,5 +299,17 @@ def generate_oval_cve(releases, outdir, cves, pkg_cache, cve_cache, oci, no_prog
292 if not no_progress:299 if not no_progress:
293 print(f'[X] Done generating OVAL CVE for packages in releases {", ".join(releases)}')300 print(f'[X] Done generating OVAL CVE for packages in releases {", ".join(releases)}')
294301
302
303def generate_json_pkg_oval(releases, output_dir, cves, cache, cve_cache, usn_database, no_progress, packages, fixed_only, expand):
304 if not no_progress:
305 print(f'[*] Generating JSON PKG for packages in releases {", ".join(releases)}')
306
307 gen = oval_lib.JSONPkgGenerator(releases, cves, packages, not no_progress, cache, usn_database, fixed_only, cve_cache, output_dir, expand)
308
309 gen.generate_json()
310
311 if not no_progress:
312 print(f'[X] Done generating JSON PKG for packages in releases {", ".join(releases)}')
313
295if __name__ == '__main__':314if __name__ == '__main__':
296 main()315 main()
diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py
index 6273d08..7105732 100755
--- a/scripts/oval_lib.py
+++ b/scripts/oval_lib.py
@@ -29,6 +29,8 @@ import tempfile
29import collections29import collections
30import glob30import glob
31import xml.etree.cElementTree as etree31import xml.etree.cElementTree as etree
32import json
33from dateutil import parser, tz
32from xml.dom import minidom34from xml.dom import minidom
33from typing import Tuple # Needed because of Python < 3.9 and to also support < 3.735from typing import Tuple # Needed because of Python < 3.9 and to also support < 3.7
3436
@@ -374,21 +376,25 @@ class Package:
374 self.rel = rel376 self.rel = rel
375 self.description = cve_lib.lookup_package_override_description(pkgname)377 self.description = cve_lib.lookup_package_override_description(pkgname)
376378
377 if not self.description:379 if rel in sources and pkgname in sources[rel]:
378 if 'description' in sources[rel][pkgname]:380 if not self.description:
379 self.description = sources[rel][pkgname]['description']381 if 'description' in sources[rel][pkgname]:
380 elif pkgname in source_map_binaries[rel] and \382 self.description = sources[rel][pkgname]['description']
381 'description' in source_map_binaries[rel][pkgname]:383 elif pkgname in source_map_binaries[rel] and \
382 self.description = source_map_binaries[rel][pkgname]['description']384 'description' in source_map_binaries[rel][pkgname]:
383 else:385 self.description = source_map_binaries[rel][pkgname]['description']
384 # Get first description found386 else:
385 if 'binaries' in sources[self.rel][self.name]:387 # Get first description found
386 for binary in sources[self.rel][self.name]['binaries']:388 if 'binaries' in sources[self.rel][self.name]:
387 if binary in source_map_binaries[self.rel] and 'description' in source_map_binaries[self.rel][binary]:389 for binary in sources[self.rel][self.name]['binaries']:
388 self.description = source_map_binaries[self.rel][binary]["description"]390 if binary in source_map_binaries[self.rel] and 'description' in source_map_binaries[self.rel][binary]:
389 break391 self.description = source_map_binaries[self.rel][binary]["description"]
390392 break
391 self.section = sources[rel][pkgname]['section']393
394 self.section = sources[rel][pkgname]['section']
395 else:
396 self.section = 'main'
397
392 self.versions_binaries = versions_binaries if versions_binaries else {}398 self.versions_binaries = versions_binaries if versions_binaries else {}
393 self.earliest_version = self.get_earliest_version()399 self.earliest_version = self.get_earliest_version()
394 self.latest_version = self.get_latest_version()400 self.latest_version = self.get_latest_version()
@@ -480,12 +486,14 @@ class Package:
480 return self.__str__()486 return self.__str__()
481487
482class USN:488class USN:
483 def __init__(self, data, cve_objs, pkgs_by_rel):489 def __init__(self, data, cve_objs, pkgs_by_rel, lp_bugs):
484 for item in ['description', 'releases', 'title', 'timestamp', 'summary', 'action', 'id', 'isummary']:490 for item in ['description', 'releases', 'title', 'timestamp', 'summary', 'action', 'id', 'isummary']:
485 if item in data:491 if item in data:
486 setattr(self, item, data[item])492 setattr(self, item, data[item])
487 else:493 else:
488 setattr(self, item, None)494 setattr(self, item, None)
495
496 self.lp_bugs = lp_bugs
489 self.cves = cve_objs497 self.cves = cve_objs
490 self.pkgs = self._generate_pkg_fixed_ver_tuple_dict(pkgs_by_rel)498 self.pkgs = self._generate_pkg_fixed_ver_tuple_dict(pkgs_by_rel)
491499
@@ -498,6 +506,13 @@ class USN:
498 tup_dict[rel][src_name] = (pkg_object, fixed_ver)506 tup_dict[rel][src_name] = (pkg_object, fixed_ver)
499 return tup_dict507 return tup_dict
500508
509 def get_pkg_fixed_version(self, package: Package):
510 if not self.is_package_present(package): return None
511 return self.pkgs[package.rel][package.name][1]
512
513 def is_package_present(self, package: Package):
514 return package.rel in self.pkgs and package.name in self.pkgs[package.rel]
515
501 def __str__(self) -> str:516 def __str__(self) -> str:
502 # return f'description: {self.description}\nid: {self.id}\ncves: {self.cves}\npkgs: {self.pkgs}\n' # TODO: remove this ugly debug print517 # return f'description: {self.description}\nid: {self.id}\ncves: {self.cves}\npkgs: {self.pkgs}\n' # TODO: remove this ugly debug print
503 return self.id518 return self.id
@@ -531,14 +546,7 @@ class OvalGenerator:
531 self.release_codename = cve_lib.release_progenitor(self.release) if cve_lib.release_progenitor(self.release) else self.release546 self.release_codename = cve_lib.release_progenitor(self.release) if cve_lib.release_progenitor(self.release) else self.release
532 self.release_codename = self.release_codename.replace('/', '_')547 self.release_codename = self.release_codename.replace('/', '_')
533 self.release_name = cve_lib.release_name(self.release)548 self.release_name = cve_lib.release_name(self.release)
534549 self.parent_releases = self._get_parent_releases(self.release)
535 self.parent_releases = list()
536 current_release = self.release
537 while(cve_lib.release_parent(current_release)):
538 current_release = cve_lib.release_parent(current_release)
539 if current_release != self.release and \
540 current_release not in self.parent_releases:
541 self.parent_releases.append(current_release)
542550
543 self.ns = 'oval:com.ubuntu.{0}'.format(self.release_codename)551 self.ns = 'oval:com.ubuntu.{0}'.format(self.release_codename)
544 self.id = 100552 self.id = 100
@@ -572,6 +580,25 @@ class OvalGenerator:
572580
573 return False581 return False
574582
583 def _get_parent_releases(self, release) -> list[str]:
584 parent_releases = list()
585 current_release = release
586 while(cve_lib.release_parent(current_release)):
587 current_release = cve_lib.release_parent(current_release)
588 if current_release != release and \
589 current_release not in parent_releases:
590 parent_releases.append(current_release)
591
592 return parent_releases
593
594 def _get_objects_including_parents(self, objects) -> dict:
595 all_objs = dict()
596 for parent_release in self.parent_releases[::-1]:
597 all_objs.update(objects[parent_release])
598
599 all_objs.update(objects[self.release])
600 return all_objs
601
575 def _add_structure(self, root) -> None:602 def _add_structure(self, root) -> None:
576 structure = {}603 structure = {}
577 for element in self.supported_oval_elements:604 for element in self.supported_oval_elements:
@@ -1508,11 +1535,7 @@ class OvalGeneratorPkg(OvalGenerator):
1508 running_kernel_id = self.definition_id1535 running_kernel_id = self.definition_id
1509 self._increase_id(is_definition=True)1536 self._increase_id(is_definition=True)
15101537
1511 all_pkgs = dict()1538 all_pkgs = self._get_objects_including_parents(self.packages)
1512 for parent_release in self.parent_releases[::-1]:
1513 all_pkgs.update(self.packages[parent_release])
1514
1515 all_pkgs.update(self.packages[self.release])
15161539
1517 for pkg in all_pkgs:1540 for pkg in all_pkgs:
1518 if self._ignore_source_package(pkg): continue1541 if self._ignore_source_package(pkg): continue
@@ -1760,12 +1783,7 @@ class OvalGeneratorCVE(OvalGenerator):
1760 accepted_releases = self.parent_releases.copy()1783 accepted_releases = self.parent_releases.copy()
1761 accepted_releases.insert(0, self.release)1784 accepted_releases.insert(0, self.release)
17621785
1763 all_cves = self.cves[self.release]1786 all_cves = self._get_objects_including_parents(self.cves)
1764 for parent_release in self.parent_releases:
1765 for cve in self.cves[parent_release]:
1766 if cve not in all_cves:
1767 all_cves[cve] = self.cves[parent_release][cve]
1768
1769 all_cves = dict(sorted(all_cves.items()))1787 all_cves = dict(sorted(all_cves.items()))
17701788
1771 for cve in all_cves:1789 for cve in all_cves:
@@ -1775,21 +1793,24 @@ class OvalGeneratorCVE(OvalGenerator):
1775 self._write_oval_xml(xml_tree, root_element)1793 self._write_oval_xml(xml_tree, root_element)
17761794
1777class OvalGeneratorUSNs(OvalGenerator):1795class OvalGeneratorUSNs(OvalGenerator):
1778 def __init__(self, releases, cve_paths, packages, progress, pkg_cache, usn_database, fixed_only=True, cve_cache=None, outdir='./', oval_format='dpkg') -> None:1796 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:
1779 super().__init__('usn', releases, cve_paths, packages, progress, pkg_cache, fixed_only, cve_cache, outdir, oval_format)1797 super().__init__('usn', releases, cve_paths, packages, progress, pkg_cache, fixed_only, cve_cache, outdir, oval_format, expand)
1780 self.usns = self._load_usns(usn_database)1798 self.usns = self._load_usns(usn_database)
17811799
1782 def _load_usns(self, usn_database):1800 def _load_usns(self, usn_database):
1783 usns = {}1801 usns = {}
1784 # go thru every USN in the JSON1802 # go thru every USN in the JSON
1785 for usn_id, usn_data in usn_database.items():1803 for usn_id, usn_data in dict(sorted(usn_database.items())).items():
1786 # take existing CVE and Package objects1804 # take existing CVE and Package objects
1787 cve_objs = {}1805 cve_objs = {}
1788 pkg_objs_by_rels = {}1806 pkg_objs_by_rels = {}
1807 lp_bugs = []
1789 for rel, info in usn_data['releases'].items():1808 for rel, info in usn_data['releases'].items():
1790 # CVE stays the same across releases1809 # CVE stays the same across releases
1791 for cve in usn_data['cves']:1810 for cve in usn_data['cves']:
1792 if cve_objs.get(cve) is None:1811 if 'launchpad' in cve:
1812 lp_bugs.append(cve)
1813 elif cve_objs.get(cve) is None:
1793 try:1814 try:
1794 cve_objs[cve] = self.cves[rel][cve]1815 cve_objs[cve] = self.cves[rel][cve]
1795 except KeyError:1816 except KeyError:
@@ -1804,7 +1825,7 @@ class OvalGeneratorUSNs(OvalGenerator):
1804 pkg_objs_by_rels[rel] = pkg_objs1825 pkg_objs_by_rels[rel] = pkg_objs
1805 # create a USN object with fields in the USN and1826 # create a USN object with fields in the USN and
1806 # corresponding CVEs and Packages1827 # corresponding CVEs and Packages
1807 usns[usn_id] = USN(usn_data, cve_objs, pkg_objs_by_rels)1828 usns[usn_id] = USN(usn_data, cve_objs, pkg_objs_by_rels, lp_bugs)
1808 return usns1829 return usns
18091830
1810 def _generate_advisory(self, usn: USN) -> etree.Element:1831 def _generate_advisory(self, usn: USN) -> etree.Element:
@@ -2702,6 +2723,183 @@ class OvalGeneratorUSN():
2702 if not os.listdir(self.tmpdir):2723 if not os.listdir(self.tmpdir):
2703 os.rmdir(self.tmpdir)2724 os.rmdir(self.tmpdir)
27042725
2726class JSONPkgGenerator(OvalGeneratorUSNs):
2727 """Class to generate JSON representation of OVAL"""
2728
2729 def __init__(self, releases, cve_paths, packages, progress, pkg_cache, usn_database,fixed_only=True, cve_cache=None, outdir='./', expand=False) -> None:
2730 self.json_data = dict()
2731 super().__init__(releases, cve_paths, packages, progress, pkg_cache, usn_database, fixed_only, cve_cache, outdir, expand=expand)
2732 self.generator_type = 'json_pkg'
2733
2734 def _generate_usn_info(self, usn: USN):
2735 return {
2736 'description': usn.description,
2737 'published_at': datetime.fromtimestamp(usn.timestamp).isoformat(timespec="seconds"),
2738 'related_cves': list(usn.cves.keys()),
2739 'related_launchpad_bugs': usn.lp_bugs
2740 }
2741
2742 def _generate_usns_info(self):
2743 usns_json = {}
2744 # USNs should probably follow the same structure as self.packages and self.cves
2745 # For now, doing this instead of self._get_objects_including_parents
2746 all_releases = self.parent_releases.copy()
2747 all_releases.append(self.release)
2748
2749 for usn_id, usn in self.usns.items():
2750 affects = False
2751 for usn_release in usn.releases:
2752 if usn_release in all_releases:
2753 affects = True
2754 break
2755
2756 if not affects: continue
2757
2758 usns_json[f'USN-{usn_id}'] = self._generate_usn_info(usn)
2759 return usns_json
2760
2761 def _generate_cve_info(self, cve: CVE) -> dict:
2762 notes = []
2763
2764 for note in cve.notes:
2765 notes.append(f'{note[0]}> {note[1]}')
2766
2767 return {
2768 'description': cve.description.strip(),
2769 'published_at': parser.parse(cve.public_date,
2770 tzinfos={'PDT': tz.gettz('America/Los_Angeles'),
2771 'PST': tz.gettz('America/Los_Angeles')
2772 }
2773 ).isoformat(timespec="seconds"),
2774 'notes': notes,
2775 'mitigation': cve.mitigation,
2776 'cvss_severity': cve.cvss[0]['baseSeverity'].lower() if cve.cvss else None,
2777 'cvss_score': float(cve.cvss[0]['baseScore']) if cve.cvss else None
2778 }
2779
2780 def _generate_cves_info(self) -> dict:
2781 cves_json = {}
2782 for cve_number, cve in self._get_objects_including_parents(self.cves).items():
2783 cves_json[cve_number] = self._generate_cve_info(cve)
2784
2785 return cves_json
2786
2787 def _generate_cve_pkg_info(self, package: Package) -> dict:
2788 pkg_cves = {}
2789 for cve in package.cves:
2790 cve_pkg_rel_entry = None
2791
2792 for pkg_rel_entry_id in cve.pkg_rel_entries:
2793 pkg_rel_entry_package, pkg_rel_entry_rel = pkg_rel_entry_id.split('/', maxsplit=1)
2794 if pkg_rel_entry_package == package.name and \
2795 pkg_rel_entry_rel == package.rel:
2796 cve_pkg_rel_entry = cve.pkg_rel_entries[pkg_rel_entry_id]
2797 break
2798
2799 if not cve_pkg_rel_entry:
2800 print(f'CVE entry for {package.name} - {package.rel} in {cve.number} not found')
2801 continue
2802
2803 pkg_cves[cve.number] = {
2804 'source_fixed_version': cve_pkg_rel_entry.fixed_version,
2805 'ubuntu_priority': cve.priority,
2806 'status': cve_pkg_rel_entry.status
2807 }
2808
2809 return pkg_cves
2810
2811 def _generate_usn_pkg_info(self, package: Package) -> dict:
2812 usns_pkg_info = dict()
2813 usns_regression_pkg_info = dict()
2814 for usn_id, usn in self.usns.items():
2815 if usn.is_package_present(package):
2816 usn_pkg_info = {
2817 'source_fixed_version': usn.get_pkg_fixed_version(package)
2818 }
2819
2820 if 'regression' in usn.title.lower():
2821 usns_regression_pkg_info[f'USN-{usn_id}'] = usn_pkg_info
2822 else:
2823 usns_pkg_info[f'USN-{usn_id}'] = usn_pkg_info
2824
2825 return usns_pkg_info, usns_regression_pkg_info
2826
2827 def _generate_package_source_info(self, package: Package, source_package_info: dict):
2828 for source_version in package.versions_binaries:
2829 if 'esm' in source_version:
2830 pocket = 'esm-apps' if package.section == 'universe' else 'esm-infra'
2831 else:
2832 _, pocket = get_pocket(self.pkg_cache, package.name, source_version, package.rel)
2833
2834 source_package_info.setdefault(source_version, {
2835 'binary_packages': dict(),
2836 'pocket': pocket.lower()
2837 })
2838 for binary_version in package.versions_binaries[source_version]:
2839 for binary in package.versions_binaries[source_version][binary_version]:
2840 source_package_info[source_version]['binary_packages'][binary] = binary_version
2841
2842 def _generate_package_info(self, package: Package) -> dict:
2843 source_package_info = dict()
2844
2845 usns_pkg_info, usns_reg_pkg_info = self._generate_usn_pkg_info(package)
2846 self._generate_package_source_info(package, source_package_info)
2847
2848 for release in self.parent_releases:
2849 if not package.name in self.packages[release]: continue
2850 parent_package = self.packages[release][package.name]
2851 self._generate_package_source_info(parent_package, source_package_info)
2852
2853 return {
2854 'source_versions': source_package_info,
2855 'ubuntu_security_notices': usns_pkg_info,
2856 'ubuntu_security_notices_regressions': usns_reg_pkg_info,
2857 'cves': self._generate_cve_pkg_info(package)
2858 }
2859
2860 def _generate_packages_info(self) -> dict:
2861 json_packages = {}
2862 for package_name, package in self._get_objects_including_parents(self.packages).items():
2863 json_packages[package_name] = self._generate_package_info(package)
2864
2865 return json_packages
2866
2867 def _generate_json_headers(self) -> dict:
2868 return {
2869 'format': '1',
2870 'published_at': datetime.now().isoformat(timespec="seconds"),
2871 'release': self.release_codename,
2872 'packages': dict(),
2873 'security_issues': dict()
2874 }
2875
2876 def _write_json(self, json_structure: dict) -> None:
2877 with open(os.path.join(self.output_dir, self.output_file), 'w+') as file:
2878 json.dump(json_structure, file)
2879
2880 def _generate_structure(self) -> dict:
2881 json_structure = self._generate_json_headers()
2882 json_structure['packages'] = self._generate_packages_info()
2883 json_structure['security_issues']['cves'] = self._generate_cves_info()
2884 json_structure['security_issues']['usns'] = self._generate_usns_info()
2885
2886 return json_structure
2887
2888 def _init_ids(self, release):
2889 self.release = release
2890 self.release_codename = cve_lib.release_progenitor(release) if cve_lib.release_progenitor(release) else release
2891 self.release_codename = self.release_codename.replace('/', '_')
2892 self.parent_releases = self._get_parent_releases(release)
2893 if self._expand_release(self.release, self.expand):
2894 self.output_file = f'com.ubuntu.{self.release.replace("/", "_")}.pkg.json'
2895 else:
2896 self.output_file = f'com.ubuntu.{self.release_codename}.pkg.json'
2897
2898 def generate_json(self) -> None:
2899 for release in self.releases:
2900 self._init_ids(release)
2901 self._write_json(self._generate_structure())
2902
2705def find_release_codename(release):2903def find_release_codename(release):
2706 return cve_lib.release_progenitor(release) if cve_lib.release_progenitor(release) else release.replace('/', '_')2904 return cve_lib.release_progenitor(release) if cve_lib.release_progenitor(release) else release.replace('/', '_')
27072905
diff --git a/test/json-gen-schema.json b/test/json-gen-schema.json
2708new file mode 1006442906new file mode 100644
index 0000000..6d1a833
--- /dev/null
+++ b/test/json-gen-schema.json
@@ -0,0 +1,123 @@
1{
2 "$schema": "http://json-schema.org/draft/2020-12/schema#",
3 "type": "object",
4 "properties": {
5 "format": {"type": "string"},
6 "published_at": {"type": "string", "format": "date-time"},
7 "release": {"type": "string"},
8 "packages": {
9 "type": "object",
10 "additionalProperties": {"$ref": "#/$defs/package"}
11 },
12 "security_issues": {
13 "type": "object",
14 "properties": {
15 "cves": {
16 "type": "object",
17 "additionalProperties": {"$ref": "#/$defs/cve"}
18 },
19 "usns": {
20 "type": "object",
21 "additionalProperties": {"$ref": "#/$defs/usn"}
22 }
23 },
24 "required": ["cves", "usns"]
25 }
26 },
27 "$defs": {
28 "package": {
29 "type": "object",
30 "properties": {
31 "source_versions": {
32 "type": "object",
33 "additionalProperties": {"$ref": "#/$defs/sourceVersion"}
34 },
35 "ubuntu_security_notices": {
36 "type": "object",
37 "additionalProperties": {"$ref": "#/$defs/securityNotice"}
38 },
39 "ubuntu_security_notices_regressions": {
40 "type": "object",
41 "additionalProperties": {"$ref": "#/$defs/securityNoticeRegression"}
42 },
43 "cves": {
44 "type": "object",
45 "additionalProperties": {"$ref": "#/$defs/cvePkg"}
46 }
47 },
48 "required": ["source_versions"]
49 },
50 "sourceVersion": {
51 "type": "object",
52 "properties": {
53 "binary_packages": {
54 "type": "object",
55 "additionalProperties": {"type": "string"}
56 },
57 "pocket": {"type": "string"}
58 },
59 "required": ["binary_packages", "pocket"]
60 },
61 "securityNotice": {
62 "type": "object",
63 "properties": {
64 "source_fixed_version": {"type": ["string", "null"]}
65 },
66 "required": ["source_fixed_version"]
67 },
68 "securityNoticeRegression": {
69 "type": "object",
70 "properties": {
71 "source_fixed_version": {"type": ["string", "null"]}
72 },
73 "required": ["source_fixed_version"]
74 },
75 "cvePkg": {
76 "type": "object",
77 "properties": {
78 "source_fixed_version": {"type": ["string", "null"]},
79 "ubuntu_priority": {
80 "type": "string",
81 "enum": ["untriaged", "negligible", "low", "medium", "high", "critical"]
82 },
83 "status": {
84 "type": "string",
85 "enum": ["not-vulnerable", "vulnerable", "fixed"]
86 }
87 },
88 "required": ["source_fixed_version", "ubuntu_priority", "status"]
89 },
90 "cve": {
91 "type": "object",
92 "properties": {
93 "description": {"type": "string"},
94 "published_at": {"type": "string", "format": "date-time"},
95 "notes": {
96 "type": "array",
97 "items": {"type": "string"}
98 },
99 "mitigation": {"type": ["string", "null"]},
100 "cvss_severity": {"type": ["string", "null"]},
101 "cvss_score": {"type": ["number", "null"]}
102 },
103 "required": ["description", "published_at", "cvss_severity", "cvss_score"]
104 },
105 "usn": {
106 "type": "object",
107 "properties": {
108 "description": {"type": "string"},
109 "published_at": {"type": "string", "format": "date-time"},
110 "related_cves": {
111 "type": "array",
112 "items": {"type": "string"}
113 },
114 "related_bugs": {
115 "type": "array",
116 "items": {"type": "string"}
117 }
118 },
119 "required": ["description", "published_at"]
120 }
121 },
122 "required": ["format", "published_at", "release", "packages", "security_issues"]
123}
diff --git a/test/test_json_generation.py b/test/test_json_generation.py
0new file mode 100644124new file mode 100644
index 0000000..a7eec6c
--- /dev/null
+++ b/test/test_json_generation.py
@@ -0,0 +1,837 @@
1import pytest
2from oval_lib import JSONPkgGenerator, Package, CVE, USN
3from dateutil import parser, tz
4from datetime import datetime
5
6def generate_mock_cve(pkgs, cve_id='CVE-0001-0001',
7 description='test',
8 priority='medium',
9 public_date='2020-08-04 17:00:00 UTC',
10 cvss = [{
11 'baseScore': '8.8',
12 'vector': '3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H',
13 'baseSeverity': 'HIGH'
14 }],
15 assigned_to='foo',
16 discovered_by='johndoe',
17 notes=[
18 ('test', 'note'),
19 ('test2', 'note2')
20 ],
21 mitigation='test',
22 references= 'http://example.com\nhttp://example2.com\n',
23 bugs='http://bug1.com\nhttp://bug2.com\n',
24 ):
25 cve = CVE(
26 number=cve_id,
27 info={
28 'Description': description,
29 'Priority': [priority],
30 'PublicDate': public_date,
31 'CVSS': cvss,
32 'Assigned-to': assigned_to,
33 'Discovered-by': discovered_by,
34 'Notes': notes,
35 'Mitigation': mitigation,
36 'References': references,
37 'Bugs': bugs
38 }
39 )
40
41 for pkg, info in pkgs.items():
42 cve.add_pkg(pkg, info['release'], info['state'], info['note'])
43
44 return cve
45
46def generate_mock_pkg(version_binaries,
47 pkgname='foo',
48 rel='jammy',
49 component='main',
50 ):
51 pkg = Package(pkgname, rel, version_binaries)
52 pkg.section = component
53
54 return pkg
55
56def 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=[]):
57 default_values = {
58 'description': 'Default Description',
59 'releases': ['jammy'],
60 'title': 'Default Title',
61 'timestamp': datetime.now().isoformat(timespec="seconds"),
62 'summary': 'Default Summary',
63 'action': 'Default Action',
64 'id': 'UNS-0001-1',
65 'isummary': 'Default ISummary'
66 }
67
68 # Update default values with provided values
69 default_values.update({
70 k: v for k, v in locals().items() if k in default_values and v is not None
71 })
72
73 # Create and return instance of MyClass with provided or default values
74 return USN(default_values, cve_objs, pkgs_by_rel, lp_bugs)
75
76
77def generate_version_binaries(source_depth, binaries, different_binary_versions=False, esm=False):
78 """
79 "0ad": {
80 "0.0.25b-1": {
81 "binaries": {
82 "0ad": {
83 "arch": [
84 "amd64",
85 "arm64",
86 "armhf"
87 ],
88 "component": "universe",
89 "version": "0.0.25b-1"
90 },
91 "0ad-dbgsym": {
92 "arch": [
93 "amd64",
94 "arm64",
95 "armhf"
96 ],
97 "component": "universe",
98 "version": "0.0.25b-1"
99 }
100 },
101 "component": "universe",
102 "pocket": "Release"
103 },
104
105 """
106 version_binaries = {}
107 for base_source_version in range(source_depth):
108 source_version = str(base_source_version)
109 if esm:
110 source_version = source_version + '+' + 'esm1'
111
112 version_binaries[source_version] = {}
113 for binary_version in range(len(binaries)):
114 binary_version_modifier = 1
115 version = str(binary_version)
116 if different_binary_versions:
117 version = version + '+' + str(binary_version_modifier)
118 binary_version_modifier += 1
119 if esm:
120 version = version + '+' + 'esm1'
121 version_binaries[source_version][version] = []
122 version_binaries[source_version][version].append(binaries[binary_version])
123
124 return version_binaries
125
126
127class EmptyJSONPkgGenerator(JSONPkgGenerator):
128 def __init__(self, releases = ['jammy']) -> None:
129 self.releases = releases
130 self.pkg_cache = None
131 self.expand = True
132 pass
133
134#### TESTS ####
135
136@pytest.mark.parametrize("mock_cve", [
137 generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001'),
138 generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', public_date='2020-08-04 17:00:00 PDT'),
139 generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', public_date='2020-08-04 17:00:00 PST'),
140 generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0004', cvss=None)
141])
142def test_generate_cve_info(mock_cve):
143 json_gen = EmptyJSONPkgGenerator()
144 generated_info = json_gen._generate_cve_info(mock_cve)
145 assert generated_info['description'] == mock_cve.description.strip()
146 assert generated_info['published_at'] == parser.parse(mock_cve.public_date,
147 tzinfos={'PDT': tz.gettz('America/Los_Angeles'),
148 'PST': tz.gettz('America/Los_Angeles')
149 }
150 ).isoformat(timespec="seconds")
151
152 notes = []
153 for note in mock_cve.notes:
154 notes.append(f"{note[0]}> {note[1]}")
155
156 assert generated_info['notes'] == notes
157 assert generated_info['mitigation'] == mock_cve.mitigation
158 assert generated_info['cvss_severity'] == (mock_cve.cvss[0]['baseSeverity'].lower() if mock_cve.cvss else None)
159 assert generated_info['cvss_score'] == (float(mock_cve.cvss[0]['baseScore']) if mock_cve.cvss else None)
160
161def test_generate_cves_info():
162 cves = {
163 'CVE-0001-0001': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001'),
164 'CVE-0001-0002': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', public_date='2020-08-04 17:00:00 PDT'),
165 'CVE-0001-0003': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', public_date='2020-08-04 17:00:00 PST'),
166 'CVE-0001-0004': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0004', cvss=None)
167 }
168
169 json_gen = EmptyJSONPkgGenerator()
170 json_gen.cves = {'jammy': {}}
171
172 for cve_id, cve in cves.items():
173 json_gen.cves[cve_id] = cve
174
175 json_gen._init_ids('jammy')
176 generated_info_cves = json_gen._generate_cves_info()
177
178 for cve_id, generated_info in generated_info_cves.items():
179 mock_cve = cves[cve_id]
180 assert generated_info['description'] == mock_cve.description.strip()
181 assert generated_info['published_at'] == parser.parse(mock_cve.public_date,
182 tzinfos={'PDT': tz.gettz('America/Los_Angeles'),
183 'PST': tz.gettz('America/Los_Angeles')
184 }
185 ).isoformat(timespec="seconds")
186 assert generated_info['notes'] == mock_cve.notes
187 assert generated_info['mitigation'] == mock_cve.mitigation
188 assert generated_info['cvss_severity'] == (mock_cve.cvss[0]['baseSeverity'].lower() if mock_cve.cvss else None)
189 assert generated_info['cvss_score'] == (float
190 (mock_cve.cvss[0]['baseScore']) if mock_cve.cvss else None)
191
192def test_generate_cves_info_parents():
193 cves_jammy = {
194 'CVE-0001-0001': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001'),
195 'CVE-0001-0002': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', public_date='2020-08-04 17:00:00 PDT'),
196 }
197
198 cves_jammy_esm_apps = {
199 'CVE-0001-0003': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', public_date='2020-08-04 17:00:00 PST'),
200 }
201
202 cves_jammy_esm_infra = {
203 'CVE-0001-0004': generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0004', cvss=None)
204 }
205
206 json_gen = EmptyJSONPkgGenerator()
207 json_gen.cves = {'jammy': {}, 'esm-apps/jammy': {}, 'esm-infra/jammy': {}}
208
209 for cve_id, cve in cves_jammy.items():
210 json_gen.cves['jammy'][cve_id] = cve
211
212 for cve_id, cve in cves_jammy_esm_apps.items():
213 json_gen.cves['esm-apps/jammy'][cve_id] = cve
214
215 for cve_id, cve in cves_jammy_esm_infra.items():
216 json_gen.cves['esm-infra/jammy'][cve_id] = cve
217
218
219 json_gen._init_ids('jammy')
220 generated_info_cves = json_gen._generate_cves_info()
221 assert len(generated_info_cves) == 2
222 for cve_id in cves_jammy.keys():
223 assert cve_id in generated_info_cves.keys()
224
225 json_gen._init_ids('esm-infra/jammy')
226 json_gen.parent_releases = ['jammy']
227 generated_info_cves = json_gen._generate_cves_info()
228 assert len(generated_info_cves) == 3
229 for cve_id in list(cves_jammy.keys()) + list(cves_jammy_esm_infra.keys()):
230 assert cve_id in generated_info_cves.keys()
231
232
233 json_gen._init_ids('esm-apps/jammy')
234 json_gen.parent_releases = ['esm-infra/jammy', 'jammy']
235 generated_info_cves = json_gen._generate_cves_info()
236 assert len(generated_info_cves) == 4
237 for cve_id in list(cves_jammy.keys()) + list(cves_jammy_esm_infra.keys()) + list(cves_jammy_esm_apps.keys()):
238 assert cve_id in generated_info_cves.keys()
239
240
241@pytest.mark.parametrize("status,note,json_status,fixed_version", [
242 ("ignored", "not for us", "vulnerable", None),
243 ("needed", "", "vulnerable", None),
244 ("needs-triage", "", "vulnerable", None),
245 ("deferred", "1-2-3", "vulnerable", None),
246 ("not-affected", "code not present", "not-vulnerable", None),
247 ("not-affected", "1.2.3", "fixed", "1.2.3"),
248 ("released", "1.2.3", "fixed", "1.2.3"),
249])
250def test_generate_cve_pkg_info(status, note, json_status, fixed_version):
251 package = generate_mock_pkg(generate_version_binaries(
252 2, ['bar', 'foo']
253 ))
254
255 cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical')
256 cve.add_pkg(package, 'jammy', status, note)
257 json_gen = EmptyJSONPkgGenerator()
258
259 info = json_gen._generate_cve_pkg_info(package)
260 info = info['CVE-0001-0001']
261
262 assert info['source_fixed_version'] == fixed_version
263 assert info['ubuntu_priority'] == 'critical'
264 assert info['status'] == json_status
265
266@pytest.mark.parametrize("usn", [
267 generate_mock_usn(
268 description='this is a test description',
269 timestamp=1708590783,
270 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
271 lp_bugs=['http://bug1.com', 'http://bug2.com']
272 ),
273 generate_mock_usn(
274 description='this is a test description',
275 timestamp=1708590783,
276 lp_bugs=['http://bug1.com', 'http://bug2.com']
277 ),
278 generate_mock_usn(
279 description='this is a test description',
280 timestamp=1708590783,
281 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}
282 ),
283])
284def test_generate_usn_info(usn):
285 # 1708590783 is ~2024-02-22T09:33:03+00:00
286 date = datetime.fromtimestamp(1708590783).isoformat(timespec="seconds")
287 json_gen = EmptyJSONPkgGenerator()
288
289 info = json_gen._generate_usn_info(usn)
290 assert info['description'] == usn.description
291 assert info['published_at'] == date
292 assert info['related_cves'] == list(usn.cves.keys())
293 assert info['related_launchpad_bugs'] == usn.lp_bugs
294
295def test_generate_usns_info():
296 date = datetime.fromtimestamp(1708590783).isoformat(timespec="seconds")
297
298 usns = [generate_mock_usn(
299 id='1000-1',
300 description='this is a test description',
301 timestamp=1708590783,
302 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
303 lp_bugs=['http://bug1.com', 'http://bug2.com']
304 ),
305 generate_mock_usn(
306 id='1000-2',
307 description='this is a test description',
308 timestamp=1708590783,
309 lp_bugs=['http://bug1.com', 'http://bug2.com']
310 ),
311 generate_mock_usn(
312 id='1000-3',
313 description='this is a test description',
314 timestamp=1708590783,
315 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}
316 )]
317
318 wrong_release_usn = generate_mock_usn(
319 id='2000-1',
320 description='this is a test description',
321 timestamp=1708590783,
322 releases=['bionic'],
323 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}
324 )
325
326 final_usns = usns.copy()
327 final_usns.append(wrong_release_usn)
328 usns_dict = dict(map(lambda usn: (usn.id, usn), final_usns))
329 json_gen = EmptyJSONPkgGenerator()
330 json_gen.usns = usns_dict
331 json_gen._init_ids('jammy')
332 info = json_gen._generate_usns_info()
333
334 assert 'USN-2000-1' not in info
335
336 for usn in usns:
337 assert f'USN-{usn.id}' in info
338 assert info[f'USN-{usn.id}']['description'] == usn.description
339 assert info[f'USN-{usn.id}']['published_at'] == date
340 assert info[f'USN-{usn.id}']['related_cves'] == list(usn.cves.keys())
341 assert info[f'USN-{usn.id}']['related_launchpad_bugs'] == usn.lp_bugs
342
343def test_generate_usns_info_parents():
344 jammy_usns = [generate_mock_usn(
345 id='1000-1',
346 description='this is a test description',
347 timestamp=1708590783,
348 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
349 lp_bugs=['http://bug1.com', 'http://bug2.com']
350 ),
351 generate_mock_usn(
352 id='1000-2',
353 description='this is a test description',
354 timestamp=1708590783,
355 lp_bugs=['http://bug1.com', 'http://bug2.com']
356 ),
357 generate_mock_usn(
358 id='1000-3',
359 description='this is a test description',
360 timestamp=1708590783,
361 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}
362 )]
363
364 jammy_infra_usns = [
365 generate_mock_usn(
366 id='2000-1',
367 description='this is a test description',
368 timestamp=1708590783,
369 releases=['esm-infra/jammy'],
370 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}
371 )
372 ]
373
374 jammy_apps_usns = [
375 generate_mock_usn(
376 id='3000-1',
377 description='this is a test description',
378 timestamp=1708590783,
379 releases=['esm-apps/jammy'],
380 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None}
381 )
382 ]
383
384 final_usns = jammy_usns + jammy_infra_usns + jammy_apps_usns
385 usns_dict = dict(map(lambda usn: (usn.id, usn), final_usns))
386 json_gen = EmptyJSONPkgGenerator()
387 json_gen.usns = usns_dict
388 json_gen._init_ids('jammy')
389 info = json_gen._generate_usns_info()
390 assert len(info.keys()) == 3
391 for usn in jammy_usns:
392 assert f'USN-{usn.id}' in info
393
394 json_gen._init_ids('esm-infra/jammy')
395 json_gen.parent_releases = ['jammy']
396 info = json_gen._generate_usns_info()
397 assert len(info.keys()) == 4
398 for usn in jammy_usns + jammy_infra_usns:
399 assert f'USN-{usn.id}' in info
400
401 json_gen._init_ids('esm-apps/jammy')
402 json_gen.parent_releases = ['esm-infra/jammy', 'jammy']
403 info = json_gen._generate_usns_info()
404 assert len(info.keys()) == 5
405 for usn in jammy_usns + jammy_infra_usns + jammy_apps_usns:
406 assert f'USN-{usn.id}' in info
407
408def test_generate_usn_pkg_info():
409 releases_1 = {
410 'jammy': {
411 'sources': {
412 'foo': {
413 'version': '1.0.0'
414 }
415 }
416 }
417 }
418
419 releases_2 = {
420 'jammy': {
421 'sources': {
422 'foo': {
423 'version': '2.0.0'
424 }
425 }
426 }
427 }
428
429 pkg = generate_mock_pkg(generate_version_binaries(
430 1, ['bar', 'foo']
431 ))
432
433 pkg_not = generate_mock_pkg(generate_version_binaries(
434 1, ['bar', 'foo']
435 ), rel='bionic')
436
437 usn = generate_mock_usn(
438 id='1000-1',
439 description='this is a test description',
440 timestamp=1708590783,
441 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
442 lp_bugs=['http://bug1.com', 'http://bug2.com'],
443 pkgs_by_rel={'jammy': {'foo': pkg}},
444 releases=releases_1
445 )
446
447 usn_regression = generate_mock_usn(
448 id='1000-2',
449 title='Regression',
450 description='this is a test description',
451 timestamp=1708590783,
452 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
453 lp_bugs=['http://bug1.com', 'http://bug2.com'],
454 pkgs_by_rel={'jammy': {'foo': pkg}},
455 releases=releases_2
456 )
457
458 json_gen = EmptyJSONPkgGenerator()
459 usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_regression]))
460 json_gen.usns = usns_dict
461
462 usn_info, usn_reg_info = json_gen._generate_usn_pkg_info(pkg)
463
464 assert 'USN-1000-1' in usn_info
465 assert 'USN-1000-2' in usn_reg_info
466 assert 'USN-1000-1' not in usn_reg_info
467 assert 'USN-1000-2' not in usn_info
468
469 assert usn_info['USN-1000-1']['source_fixed_version'] == '1.0.0'
470 assert usn_reg_info['USN-1000-2']['source_fixed_version'] == '2.0.0'
471
472 usn_info, usn_reg_info = json_gen._generate_usn_pkg_info(pkg_not)
473
474 assert 'USN-1000-1' not in usn_info
475 assert 'USN-1000-2' not in usn_reg_info
476 assert 'USN-1000-1' not in usn_reg_info
477 assert 'USN-1000-2' not in usn_info
478
479
480@pytest.mark.parametrize("package,pocket", [
481 (generate_mock_pkg(generate_version_binaries(
482 1, ['bar', 'foo']
483 )), "security"),
484 (generate_mock_pkg(generate_version_binaries(
485 2, ['bar', 'foo'], different_binary_versions=True
486 )), "release"),
487 (generate_mock_pkg(generate_version_binaries(
488 3, ['bar', 'foo'], esm=True,
489 )), "esm-infra"),
490 (generate_mock_pkg(generate_version_binaries(
491 4, ['bar', 'foo'], esm=True
492 ), component='universe'), "esm-apps"),
493])
494def test_generate_package_source_info(mocker, package, pocket):
495 mocker.patch('oval_lib.get_pocket', return_value=('',pocket))
496 json_gen = EmptyJSONPkgGenerator()
497 info = dict()
498 json_gen._generate_package_source_info(package, info)
499
500 for source_version in package.versions_binaries:
501 assert source_version in info
502 for binary_version in package.versions_binaries[source_version]:
503 for binary in package.versions_binaries[source_version][binary_version]:
504 assert binary in info[source_version]['binary_packages']
505 assert info[source_version]['binary_packages'][binary] == binary_version
506
507 assert info[source_version]['pocket'] == pocket
508
509def test_generate_package_info(mocker):
510 mocker.patch('oval_lib.get_pocket', return_value=('','release'))
511
512 releases_1 = {
513 'jammy': {
514 'sources': {
515 'foo': {
516 'version': '1.0.0'
517 }
518 }
519 }
520 }
521
522 releases_2 = {
523 'bionic': {
524 'sources': {
525 'bar': {
526 'version': '2.0.0'
527 }
528 }
529 }
530 }
531
532 pkg = generate_mock_pkg(generate_version_binaries(
533 1, ['bar', 'foo']
534 ))
535
536 pkg2 = generate_mock_pkg(generate_version_binaries(
537 2, ['bar', 'foo']
538 ), rel='bionic', pkgname='bar')
539
540 usn = generate_mock_usn(
541 id='1000-1',
542 description='this is a test description',
543 timestamp=1708590783,
544 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
545 lp_bugs=['http://bug1.com', 'http://bug2.com'],
546 pkgs_by_rel={'jammy': {'foo': pkg}},
547 releases=releases_1
548 )
549
550 usn_regression = generate_mock_usn(
551 id='1000-2',
552 title='Regression',
553 description='this is a test description',
554 timestamp=1708590783,
555 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
556 lp_bugs=['http://bug1.com', 'http://bug2.com'],
557 pkgs_by_rel={'bionic': {'bar': pkg2}},
558 releases=releases_2
559 )
560
561
562 cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical')
563 cve2 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', priority='medium')
564
565 cve.add_pkg(pkg, 'jammy', 'needed', '')
566 cve2.add_pkg(pkg2, 'bionic', 'not-affected', '1')
567 json_gen = EmptyJSONPkgGenerator()
568 usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_regression]))
569 json_gen.usns = usns_dict
570 json_gen.cves = dict()
571 json_gen.cves['CVE-0001-0001'] = cve
572 json_gen.cves['CVE-0001-0002'] = cve2
573
574 json_gen.packages = {}
575 json_gen.packages['jammy'] = {
576 'foo': pkg
577 }
578
579 json_gen.packages['bionic'] = {
580 'bar': pkg2
581 }
582
583 json_gen._init_ids('jammy')
584 info = json_gen._generate_package_info(pkg)
585 assert '0' in info['source_versions']
586 assert 'USN-1000-1' in info['ubuntu_security_notices']
587 assert len(info['ubuntu_security_notices_regressions']) == 0
588 assert 'CVE-0001-0001' in info['cves']
589 assert 'CVE-0001-0002' not in info['cves']
590
591 json_gen._init_ids('bionic')
592 info = json_gen._generate_package_info(pkg2)
593 assert '0' in info['source_versions']
594 assert 'USN-1000-2' in info['ubuntu_security_notices_regressions']
595 assert len(info['ubuntu_security_notices']) == 0
596 assert 'CVE-0001-0002' in info['cves']
597 assert 'CVE-0001-0001' not in info['cves']
598
599def test_generate_packages_info(mocker):
600 mocker.patch('oval_lib.get_pocket', return_value=('','release'))
601
602 releases_1 = {
603 'jammy': {
604 'sources': {
605 'foo': {
606 'version': '1.0.0'
607 }
608 }
609 }
610 }
611
612 releases_2 = {
613 'bionic': {
614 'sources': {
615 'bar': {
616 'version': '2.0.0'
617 }
618 }
619 }
620 }
621
622 pkg = generate_mock_pkg(generate_version_binaries(
623 1, ['bar', 'foo']
624 ))
625
626 pkg2 = generate_mock_pkg(generate_version_binaries(
627 2, ['bar', 'foo']
628 ), rel='bionic', pkgname='bar')
629
630 usn = generate_mock_usn(
631 id='1000-1',
632 description='this is a test description',
633 timestamp=1708590783,
634 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
635 lp_bugs=['http://bug1.com', 'http://bug2.com'],
636 pkgs_by_rel={'jammy': {'foo': pkg}},
637 releases=releases_1
638 )
639
640 usn_regression = generate_mock_usn(
641 id='1000-2',
642 title='Regression',
643 description='this is a test description',
644 timestamp=1708590783,
645 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
646 lp_bugs=['http://bug1.com', 'http://bug2.com'],
647 pkgs_by_rel={'bionic': {'bar': pkg2}},
648 releases=releases_2
649 )
650
651
652 cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical')
653 cve2 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', priority='medium')
654
655 cve.add_pkg(pkg, 'jammy', 'needed', '')
656 cve2.add_pkg(pkg2, 'bionic', 'not-affected', '1')
657 json_gen = EmptyJSONPkgGenerator()
658 usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_regression]))
659 json_gen.usns = usns_dict
660 json_gen.cves = dict()
661 json_gen.cves['CVE-0001-0001'] = cve
662 json_gen.cves['CVE-0001-0002'] = cve2
663 json_gen.packages = {
664 'jammy': {
665 'foo': pkg
666 },
667 'bionic': {
668 'bar': pkg2
669 }
670 }
671
672 json_gen._init_ids('jammy')
673 info = json_gen._generate_packages_info()
674 assert 'foo' in info
675 assert 'bar' not in info
676
677 json_gen._init_ids('bionic')
678 info = json_gen._generate_packages_info()
679 assert 'foo' not in info
680 assert 'bar' in info
681
682def test_generate_packages_info_parents(mocker):
683 mocker.patch('oval_lib.get_pocket', return_value=('','release'))
684
685 releases_jammy = {
686 'jammy': {
687 'sources': {
688 'foo': {
689 'version': '1.0.0'
690 }
691 }
692 }
693 }
694
695 releases_infra_jammy = {
696 'esm-infra/jammy': {
697 'sources': {
698 'bar': {
699 'version': '2.0.0'
700 }
701 }
702 }
703 }
704
705 releases_apps_jammy = {
706 'esm-apps/jammy': {
707 'sources': {
708 'dodo': {
709 'version': '3.0.0'
710 }
711 }
712 }
713 }
714
715 pkg = generate_mock_pkg(generate_version_binaries(
716 1, ['bar', 'foo']
717 ))
718
719 pkg2 = generate_mock_pkg(generate_version_binaries(
720 2, ['bar', 'fofa']
721 ), rel='esm-infra/jammy', pkgname='bar')
722
723
724 pkg3 = generate_mock_pkg(generate_version_binaries(
725 2, ['dodo', 'foo']
726 ), rel='esm-apps/jammy', pkgname='dodo')
727
728 pkg4 = generate_mock_pkg(generate_version_binaries(
729 3, ['bar', 'foo']
730 ), rel='esm-apps/jammy')
731
732
733 usn = generate_mock_usn(
734 id='1000-1',
735 description='this is a test description',
736 timestamp=1708590783,
737 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
738 lp_bugs=['http://bug1.com', 'http://bug2.com'],
739 pkgs_by_rel={'jammy': {'foo': pkg}},
740 releases=releases_jammy
741 )
742
743 usn_infra = generate_mock_usn(
744 id='2000-1',
745 title='Infra',
746 description='this is a test description',
747 timestamp=1708590783,
748 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
749 lp_bugs=['http://bug1.com', 'http://bug2.com'],
750 pkgs_by_rel={'esm-infra/jammy': {'bar': pkg2}},
751 releases=releases_infra_jammy
752 )
753
754 usn_apps = generate_mock_usn(
755 id='3000-1',
756 title='Apps',
757 description='this is a test description',
758 timestamp=1708590783,
759 cve_objs={'CVE-0001-0001': None, 'CVE-0001-0002': None},
760 lp_bugs=['http://bug1.com', 'http://bug2.com'],
761 pkgs_by_rel={'esm-apps/jammy': {'dodo': pkg3}},
762 releases=releases_apps_jammy
763 )
764
765 cve = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0001', priority='critical')
766 cve2 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0002', priority='medium')
767 cve3 = generate_mock_cve(pkgs=dict(), cve_id='CVE-0001-0003', priority='medium')
768
769 cve.add_pkg(pkg, 'jammy', 'needed', '')
770 cve.add_pkg(pkg4, 'esm-apps/jammy', 'not-affected', '4')
771 cve2.add_pkg(pkg2, 'esm-infra/jammy', 'not-affected', '1')
772 cve3.add_pkg(pkg3, 'esm-apps/jammy', 'released', '2')
773
774 json_gen = EmptyJSONPkgGenerator()
775 usns_dict = dict(map(lambda usn: (usn.id, usn), [usn, usn_infra, usn_apps]))
776 json_gen.usns = usns_dict
777 json_gen.cves = {'jammy': {}, 'esm-apps/jammy': {}, 'esm-infra/jammy': {}}
778 json_gen.cves['jammy']['CVE-0001-0001'] = cve
779 json_gen.cves['esm-infra/jammy']['CVE-0001-0002'] = cve2
780 json_gen.cves['esm-apps/jammy']['CVE-0001-0003'] = cve3
781
782 json_gen.packages = {
783 'jammy': {
784 'foo': pkg
785 },
786 'esm-infra/jammy': {
787 'bar': pkg2
788 },
789 'esm-apps/jammy': {
790 'dodo': pkg3,
791 'foo': pkg4
792 }
793 }
794
795 json_gen._init_ids('jammy')
796 info = json_gen._generate_packages_info()
797 assert 'foo' in info
798 assert 'bar' not in info
799 assert 'dodo' not in info
800 assert info['foo']['cves']['CVE-0001-0001']['status'] == 'vulnerable'
801
802 json_gen._init_ids('esm-infra/jammy')
803 json_gen.parent_releases = ['jammy']
804 info = json_gen._generate_packages_info()
805 assert 'foo' in info
806 assert 'bar' in info
807 assert 'dodo' not in info
808 assert info['foo']['cves']['CVE-0001-0001']['status'] == 'vulnerable'
809 assert info['bar']['cves']['CVE-0001-0002']['status'] == 'fixed'
810 assert info['bar']['ubuntu_security_notices']['USN-2000-1']['source_fixed_version'] == '2.0.0'
811
812 json_gen._init_ids('esm-apps/jammy')
813 json_gen.parent_releases = ['esm-infra/jammy', 'jammy']
814 info = json_gen._generate_packages_info()
815 assert 'foo' in info
816 assert 'bar' in info
817 assert 'dodo' in info
818 assert info['foo']['cves']['CVE-0001-0001']['status'] == 'fixed'
819 assert info['bar']['cves']['CVE-0001-0002']['status'] == 'fixed'
820 assert info['dodo']['cves']['CVE-0001-0003']['status'] == 'fixed'
821 assert info['foo']['cves']['CVE-0001-0001']['source_fixed_version'] == '4'
822 assert info['dodo']['ubuntu_security_notices']['USN-3000-1']['source_fixed_version'] == '3.0.0'
823
824@pytest.mark.parametrize("expand,release,filename", [
825 (False, 'esm-apps/bionic', 'com.ubuntu.bionic.pkg.json'),
826 (True, 'esm-apps/bionic', 'com.ubuntu.esm-apps_bionic.pkg.json'),
827 (False, 'esm-infra/bionic', 'com.ubuntu.bionic.pkg.json'),
828 (True, 'esm-infra/bionic', 'com.ubuntu.esm-infra_bionic.pkg.json'),
829 (False, 'bionic', 'com.ubuntu.bionic.pkg.json'),
830 (True, 'bionic', 'com.ubuntu.bionic.pkg.json')
831])
832def test_expand(expand, release, filename):
833 json_gen = EmptyJSONPkgGenerator()
834 json_gen.expand = expand
835 json_gen._init_ids(release)
836
837 assert json_gen.output_file == filename

Subscribers

People subscribed via source and target branches