From b4c25eb532619c92603d944febb1397048db0ff8 Mon Sep 17 00:00:00 2001 From: Lucas Moura Date: Thu, 10 Oct 2024 17:20:25 -0300 Subject: [PATCH] cli: change vulnerability applied fixes message Instead of showing the count of applied fixes per package, we are now switching to count be vulnerability instead. The main reason is to make the count consistent with the other summary information we have on pro vulnerability list --- features/cli/vulnerability_list.feature | 34 ++++++++ ..._pro_security_vulnerabilities_common_v1.py | 56 ++++++++++++ .../security/vulnerabilities/_common/v1.py | 85 ++++++++++--------- uaclient/cli/vulnerability/list.py | 8 +- 4 files changed, 141 insertions(+), 42 deletions(-) diff --git a/features/cli/vulnerability_list.feature b/features/cli/vulnerability_list.feature index eb418e0c09..296ffde624 100644 --- a/features/cli/vulnerability_list.feature +++ b/features/cli/vulnerability_list.feature @@ -142,3 +142,37 @@ Feature: CLI vulnerability list command Examples: ubuntu | release | machine_type | | xenial | lxd-container | + + @slow + Scenario Outline: CLI vulnerability list after upgrade + Given a `` `` machine with ubuntu-advantage-tools installed + When I push static file `security_issues_xenial.json` to machine + And I run `pro vulnerability list --data-file=/tmp/security_issues_xenial.json` as non-root + And I remove colors from output + And I remove links from output + Then stdout contains substring: + """ + Vulnerabilities with applied fixes: + 861 applied via Ubuntu Security (32 high, 494 medium, 322 low, 13 negligible) + + Vulnerabilities with fixes available: + 461 fixable via Ubuntu Pro (10 high, 293 medium, 136 low, 22 negligible) + 1 fixable via Ubuntu Security (1 medium) + """ + When I attach `contract_token` with sudo + And I apt upgrade + When I run `pro vulnerability list --data-file=/tmp/security_issues_xenial.json` as non-root + And I remove colors from output + And I remove links from output + Then stdout contains substring: + """ + No CVEs found that affects this system + + Vulnerabilities with applied fixes: + 461 applied via Ubuntu Pro (10 high, 293 medium, 136 low, 22 negligible) + 862 applied via Ubuntu Security (32 high, 495 medium, 322 low, 13 negligible) + """ + + Examples: ubuntu + | release | machine_type | + | xenial | lxd-container | diff --git a/uaclient/api/tests/test_api_u_pro_security_vulnerabilities_common_v1.py b/uaclient/api/tests/test_api_u_pro_security_vulnerabilities_common_v1.py index 37b2747f97..65ad1768ad 100644 --- a/uaclient/api/tests/test_api_u_pro_security_vulnerabilities_common_v1.py +++ b/uaclient/api/tests/test_api_u_pro_security_vulnerabilities_common_v1.py @@ -5,6 +5,7 @@ from uaclient.api.u.pro.security.vulnerabilities._common.v1 import ( ProManifestSourcePackage, SourcePackages, + VulnerabilitiesAlreadyFixed, VulnerabilityParser, VulnerabilityStatus, _get_source_package_from_vulnerabilities_data, @@ -414,3 +415,58 @@ def test_parse_manifest_file( vulnerabilities_data=vulnerabilities_data, ).get() ) + + +class TestVulnerabilitiesAlreadyFixed: + + @pytest.mark.parametrize( + "vulnerabilities,expected_result", + ( + ( + [ + ("CVE-1234-5", "ubuntu_pro", "high"), + ("CVE-1234-5", "ubuntu_security", "high"), + ("CVE-1552-5", "ubuntu_pro", "medium"), + ("CVE-1382-5", "ubuntu_security", "low"), + ], + { + "count": { + "ubuntu_pro": 2, + "ubuntu_security": 2, + }, + "info": { + "ubuntu_pro": {"high": 1, "medium": 1}, + "ubuntu_security": {"high": 1, "low": 1}, + }, + }, + ), + ( + [ + ("USN-1234-5", "ubuntu_pro"), + ("USN-1234-5", "ubuntu_security"), + ("USN-1552-5", "ubuntu_pro"), + ("USN-1382-5", "ubuntu_security"), + ], + { + "count": { + "ubuntu_pro": 2, + "ubuntu_security": 2, + }, + "info": { + "ubuntu_pro": {}, + "ubuntu_security": {}, + }, + }, + ), + ), + ) + def test_vulnerabilities_already_fixed_to_dict( + self, + vulnerabilities, + expected_result, + ): + vulnerabilities_already_fixed = VulnerabilitiesAlreadyFixed() + for vulnerability in vulnerabilities: + vulnerabilities_already_fixed.add_vulnerability(*vulnerability) + + assert expected_result == vulnerabilities_already_fixed.to_dict() diff --git a/uaclient/api/u/pro/security/vulnerabilities/_common/v1.py b/uaclient/api/u/pro/security/vulnerabilities/_common/v1.py index 0ab942a818..481e4e401f 100644 --- a/uaclient/api/u/pro/security/vulnerabilities/_common/v1.py +++ b/uaclient/api/u/pro/security/vulnerabilities/_common/v1.py @@ -328,6 +328,47 @@ def _get_vulnerability_fix_status( ) +class VulnerabilitiesAlreadyFixed: + def __init__(self): + self.vulns = {} + + def add_vulnerability( + self, + vuln_name: str, + vuln_pocket: str, + vuln_priority: Optional[str] = None, + ): + if vuln_pocket not in self.vulns: + self.vulns[vuln_pocket] = { + "issues": set(), + "priority_count": {}, + } + + if vuln_name not in self.vulns[vuln_pocket]["issues"]: + self.vulns[vuln_pocket]["issues"].add(vuln_name) + + if vuln_priority: + if vuln_priority in self.vulns[vuln_pocket]["priority_count"]: + self.vulns[vuln_pocket]["priority_count"][ + vuln_priority + ] += 1 + else: + self.vulns[vuln_pocket]["priority_count"][ + vuln_priority + ] = 1 + + def to_dict(self): + dict_repr = { + "count": {}, + "info": {}, + } # type: Dict[str, Dict[str, Any]] + for pocket, info in self.vulns.items(): + dict_repr["count"][pocket] = len(info["issues"]) + dict_repr["info"][pocket] = info["priority_count"] + + return dict_repr + + class VulnerabilityParser(metaclass=abc.ABCMeta): vulnerability_type = None # type: str @@ -510,39 +551,13 @@ def _list_binary_packages(self, installed_pkgs_by_source: Dict[str, Any]): ) in sorted(binary_pkgs.items()): yield source_pkg, binary_pkg_name, binary_installed_version - def _add_info_to_fixed_vulnerabilities_count( - self, - pocket: str, - fixed_vulnerability_info: Dict[str, Any], - vulnerability_info: Dict[str, Any], - ) -> Dict[str, Any]: - ubuntu_priority = vulnerability_info.get("ubuntu_priority") - - if not ubuntu_priority: - return fixed_vulnerability_info - - if pocket not in fixed_vulnerability_info: - fixed_vulnerability_info[pocket] = {ubuntu_priority: 1} - elif ubuntu_priority not in fixed_vulnerability_info[pocket]: - fixed_vulnerability_info[pocket][ubuntu_priority] = 1 - else: - fixed_vulnerability_info[pocket][ubuntu_priority] += 1 - - return fixed_vulnerability_info - def get_vulnerabilities_for_installed_pkgs( self, vulnerabilities_data: Dict[str, Any], installed_pkgs_by_source: Dict[str, Dict[str, str]], ): vulnerabilities = {} # type: Dict[str, Any] - applied_fixes_count = { - "count": { - "ubuntu_security": 0, - "ubuntu_pro": 0, - }, - "info": {}, - } + vulnerabilities_already_fixed = VulnerabilitiesAlreadyFixed() affected_pkgs = vulnerabilities_data.get("packages", {}) vulns_info = vulnerabilities_data.get("security_issues", {}).get( @@ -651,16 +666,10 @@ def get_vulnerabilities_for_installed_pkgs( if pocket in ("release", "updates", "security") else "ubuntu_pro" ) - applied_fixes_count["count"][ - ubuntu_pocket_translation - ] += 1 - - applied_fixes_count["info"] = ( - self._add_info_to_fixed_vulnerabilities_count( - ubuntu_pocket_translation, - applied_fixes_count["info"], - vuln_info, - ) + vulnerabilities_already_fixed.add_vulnerability( + vuln_name=vuln_name, + vuln_pocket=ubuntu_pocket_translation, + vuln_priority=vuln_info.get("ubuntu_priority"), ) continue @@ -671,7 +680,7 @@ def get_vulnerabilities_for_installed_pkgs( ), vulnerabilities_info={ "vulnerabilities": vulnerabilities, - "applied_fixes_count": applied_fixes_count, + "applied_fixes_count": vulnerabilities_already_fixed.to_dict(), }, ) diff --git a/uaclient/cli/vulnerability/list.py b/uaclient/cli/vulnerability/list.py index 679cedac8e..2be00aa36a 100644 --- a/uaclient/cli/vulnerability/list.py +++ b/uaclient/cli/vulnerability/list.py @@ -202,7 +202,7 @@ def _create_fixable_cves_count(vulnerabilities) -> str: vulnerabilities ) - for pocket, pocket_info in fixable_vulnerabilities_info.items(): + for pocket, pocket_info in sorted(fixable_vulnerabilities_info.items()): if not pocket_info["count"]: continue @@ -227,7 +227,7 @@ def _create_fixable_usns_count(vulnerabilities) -> str: fixable_vulnerabilities_info = _get_info_from_vulnerabilities( vulnerabilities ) - for pocket, pocket_info in fixable_vulnerabilities_info.items(): + for pocket, pocket_info in sorted(fixable_vulnerabilities_info.items()): if not pocket_info["count"]: continue @@ -282,7 +282,7 @@ def _create_unfixable_usns_count(vulnerabilities) -> str: def _create_already_fixed_cves_count(applied_fixes_count) -> str: if any(count for pocket, count in applied_fixes_count["count"].items()): content = [] - for pocket, count in applied_fixes_count["count"].items(): + for pocket, count in sorted(applied_fixes_count["count"].items()): if not count: continue @@ -310,7 +310,7 @@ def _create_already_fixed_cves_count(applied_fixes_count) -> str: def _create_already_fixed_usns_count(applied_fixes_count) -> str: if any(count for pocket, count in applied_fixes_count["count"].items()): content = [] - for pocket, count in applied_fixes_count["count"].items(): + for pocket, count in sorted(applied_fixes_count["count"].items()): if not count: continue