from collections import defaultdict import click from safety.formatter import FormatterAPI from safety.output_utils import build_announcements_section_content, format_vulnerability, \ build_report_brief_section, get_final_brief_license, add_empty_line, get_final_brief, build_remediation_section, \ build_primary_announcement, format_unpinned_vulnerabilities from safety.util import get_primary_announcement, get_basic_announcements, is_ignore_unpinned_mode, \ get_remediations_count from typing import List, Dict, Tuple, Any class TextReport(FormatterAPI): """Basic report, intented to be used for terminals with < 80 columns""" SMALL_DIVIDER_SECTIONS = '+' + '=' * 78 + '+' TEXT_REPORT_BANNER = SMALL_DIVIDER_SECTIONS + '\n' + r""" /$$$$$$ /$$ /$$__ $$ | $$ /$$$$$$$ /$$$$$$ | $$ \__//$$$$$$ /$$$$$$ /$$ /$$ /$$_____/ |____ $$| $$$$ /$$__ $$|_ $$_/ | $$ | $$ | $$$$$$ /$$$$$$$| $$_/ | $$$$$$$$ | $$ | $$ | $$ \____ $$ /$$__ $$| $$ | $$_____/ | $$ /$$| $$ | $$ /$$$$$$$/| $$$$$$$| $$ | $$$$$$$ | $$$$/| $$$$$$$ |_______/ \_______/|__/ \_______/ \___/ \____ $$ /$$ | $$ | $$$$$$/ by safetycli.com \______/ """ + SMALL_DIVIDER_SECTIONS def __build_announcements_section(self, announcements: List[Dict]) -> List[str]: """ Build the announcements section of the report. Args: announcements (List[Dict]): List of announcement dictionaries. Returns: List[str]: Formatted announcements section. """ announcements_table = [] basic_announcements = get_basic_announcements(announcements) if basic_announcements: announcements_content = click.unstyle(build_announcements_section_content(basic_announcements, columns=80)) announcements_table = [add_empty_line(), ' ANNOUNCEMENTS', add_empty_line(), announcements_content, add_empty_line(), self.SMALL_DIVIDER_SECTIONS] return announcements_table def render_vulnerabilities( self, announcements: List[Dict], vulnerabilities: List[Dict], remediations: Dict[str, Any], full: bool, packages: List[Dict], fixes: Tuple = () ) -> str: """ Render the vulnerabilities section of the report. Args: announcements (List[Dict]): List of announcement dictionaries. vulnerabilities (List[Dict]): List of vulnerability dictionaries. remediations (Dict[str, Any]): Remediation data. full (bool): Flag indicating full report. packages (List[Dict]): List of package dictionaries. fixes (Tuple, optional): Iterable of fixes. Returns: str: Rendered vulnerabilities report. """ primary_announcement = get_primary_announcement(announcements) remediation_section = [click.unstyle(rem) for rem in build_remediation_section(remediations, columns=80)] end_content = [] if primary_announcement: end_content = [add_empty_line(), build_primary_announcement(primary_announcement, columns=80, only_text=True), self.SMALL_DIVIDER_SECTIONS] announcement_section = self.__build_announcements_section(announcements) ignored = {} total_ignored = 0 unpinned_packages = defaultdict(list) raw_vulns = [] for n, vuln in enumerate(vulnerabilities): if vuln.ignored: total_ignored += 1 ignored[vuln.package_name] = ignored.get(vuln.package_name, 0) + 1 if is_ignore_unpinned_mode(version=vuln.analyzed_version) and not full: unpinned_packages[vuln.package_name].append(vuln) continue raw_vulns.append(vuln) report_brief_section = click.unstyle( build_report_brief_section(columns=80, primary_announcement=primary_announcement, vulnerabilities_found=max(0, len(vulnerabilities)-total_ignored), vulnerabilities_ignored=total_ignored, remediations_recommended=remediations)) table = [self.TEXT_REPORT_BANNER] + announcement_section + [ report_brief_section, '', self.SMALL_DIVIDER_SECTIONS, ] if vulnerabilities: table += [" VULNERABILITIES FOUND", self.SMALL_DIVIDER_SECTIONS] if unpinned_packages: table.append('') table.extend(map(click.unstyle, format_unpinned_vulnerabilities(unpinned_packages, columns=80))) if not raw_vulns: table.append('') for vuln in raw_vulns: table.append('\n' + format_vulnerability(vuln, full, only_text=True, columns=80)) final_brief = click.unstyle(get_final_brief(len(vulnerabilities), remediations, ignored, total_ignored, kwargs={'columns': 80})) table += [add_empty_line(), self.SMALL_DIVIDER_SECTIONS] + remediation_section + ['', final_brief, '', self.SMALL_DIVIDER_SECTIONS] + end_content else: table += [add_empty_line(), " No known security vulnerabilities found.", add_empty_line(), self.SMALL_DIVIDER_SECTIONS] + end_content return "\n".join( table ) def render_licenses(self, announcements: List[Dict], licenses: List[Dict]) -> str: """ Render the licenses section of the report. Args: announcements (List[Dict]): List of announcement dictionaries. licenses (List[Dict]): List of license dictionaries. Returns: str: Rendered licenses report. """ unique_license_types = set([lic['license'] for lic in licenses]) report_brief_section = click.unstyle( build_report_brief_section(columns=80, primary_announcement=get_primary_announcement(announcements), licenses_found=len(unique_license_types))) packages_licenses = licenses announcements_table = self.__build_announcements_section(announcements) final_brief = click.unstyle( get_final_brief_license(unique_license_types, kwargs={'columns': 80})) table = [self.TEXT_REPORT_BANNER] + announcements_table + [ report_brief_section, self.SMALL_DIVIDER_SECTIONS, " LICENSES", self.SMALL_DIVIDER_SECTIONS, add_empty_line(), ] if not packages_licenses: table.append(" No packages licenses found.") table += [final_brief, add_empty_line(), self.SMALL_DIVIDER_SECTIONS] return "\n".join(table) for pkg_license in packages_licenses: text = " {0}, version {1}, license {2}\n".format(pkg_license['package'], pkg_license['version'], pkg_license['license']) table.append(text) table += [final_brief, add_empty_line(), self.SMALL_DIVIDER_SECTIONS] return "\n".join(table) def render_announcements(self, announcements: List[Dict]) -> str: """ Render the announcements section of the report. Args: announcements (List[Dict]): List of announcement dictionaries. Returns: str: Rendered announcements section. """ rows = self.__build_announcements_section(announcements) rows.insert(0, self.SMALL_DIVIDER_SECTIONS) return '\n'.join(rows)