diff --git a/cycode/cli/app.py b/cycode/cli/app.py index aed0e172..5c83be9e 100644 --- a/cycode/cli/app.py +++ b/cycode/cli/app.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path from typing import Annotated, Optional import typer @@ -6,8 +7,9 @@ from cycode import __version__ from cycode.cli.apps import ai_remediation, auth, configure, ignore, report, scan, status -from cycode.cli.cli_types import OutputTypeOption +from cycode.cli.cli_types import ExportTypeOption, OutputTypeOption from cycode.cli.consts import CLI_CONTEXT_SETTINGS +from cycode.cli.printers import ConsolePrinter from cycode.cli.user_settings.configuration_manager import ConfigurationManager from cycode.cli.utils.progress_bar import SCAN_PROGRESS_BAR_SECTIONS, get_progress_bar from cycode.cli.utils.sentry import add_breadcrumb, init_sentry @@ -44,7 +46,14 @@ def check_latest_version_on_close(ctx: typer.Context) -> None: version_checker.check_and_notify_update(current_version=__version__, use_cache=should_use_cache) +def export_if_needed_on_close(ctx: typer.Context) -> None: + printer = ctx.obj.get('console_printer') + if printer.is_recording: + printer.export() + + _COMPLETION_RICH_HELP_PANEL = 'Completion options' +_EXPORT_RICH_HELP_PANEL = 'Export options' @app.callback() @@ -64,6 +73,27 @@ def app_callback( Optional[str], typer.Option(hidden=True, help='Characteristic JSON object that lets servers identify the application.'), ] = None, + export_type: Annotated[ + ExportTypeOption, + typer.Option( + '--export-type', + case_sensitive=False, + help='Specify the export type. ' + 'HTML and SVG will export terminal output and rely on --output option. ' + 'JSON always exports JSON.', + rich_help_panel=_EXPORT_RICH_HELP_PANEL, + ), + ] = ExportTypeOption.JSON, + export_file: Annotated[ + Optional[Path], + typer.Option( + '--export-file', + help='Export file. Path to the file where the export will be saved. ', + dir_okay=False, + writable=True, + rich_help_panel=_EXPORT_RICH_HELP_PANEL, + ), + ] = None, _: Annotated[ Optional[bool], typer.Option( @@ -104,6 +134,11 @@ def app_callback( ctx.obj['progress_bar'] = get_progress_bar(hidden=no_progress_meter, sections=SCAN_PROGRESS_BAR_SECTIONS) + ctx.obj['export_type'] = export_type + ctx.obj['export_file'] = export_file + ctx.obj['console_printer'] = ConsolePrinter(ctx) + ctx.call_on_close(lambda: export_if_needed_on_close(ctx)) + if user_agent: user_agent_option = UserAgentOptionScheme().loads(user_agent) CycodeClientBase.enrich_user_agent(user_agent_option.user_agent_suffix) diff --git a/cycode/cli/apps/ai_remediation/apply_fix.py b/cycode/cli/apps/ai_remediation/apply_fix.py index e0c2599b..bd840411 100644 --- a/cycode/cli/apps/ai_remediation/apply_fix.py +++ b/cycode/cli/apps/ai_remediation/apply_fix.py @@ -4,11 +4,10 @@ from patch_ng import fromstring from cycode.cli.models import CliResult -from cycode.cli.printers import ConsolePrinter def apply_fix(ctx: typer.Context, diff: str, is_fix_available: bool) -> None: - printer = ConsolePrinter(ctx) + printer = ctx.obj.get('console_printer') if not is_fix_available: printer.print_result(CliResult(success=False, message='Fix is not available for this violation')) return diff --git a/cycode/cli/apps/ai_remediation/print_remediation.py b/cycode/cli/apps/ai_remediation/print_remediation.py index c0109341..92272b76 100644 --- a/cycode/cli/apps/ai_remediation/print_remediation.py +++ b/cycode/cli/apps/ai_remediation/print_remediation.py @@ -3,11 +3,10 @@ from cycode.cli.console import console from cycode.cli.models import CliResult -from cycode.cli.printers import ConsolePrinter def print_remediation(ctx: typer.Context, remediation_markdown: str, is_fix_available: bool) -> None: - printer = ConsolePrinter(ctx) + printer = ctx.obj.get('console_printer') if printer.is_json_printer: data = {'remediation': remediation_markdown, 'is_fix_available': is_fix_available} printer.print_result(CliResult(success=True, message='Remediation fetched successfully', data=data)) diff --git a/cycode/cli/apps/auth/auth_command.py b/cycode/cli/apps/auth/auth_command.py index 8150be01..a402b0c2 100644 --- a/cycode/cli/apps/auth/auth_command.py +++ b/cycode/cli/apps/auth/auth_command.py @@ -4,13 +4,13 @@ from cycode.cli.exceptions.handle_auth_errors import handle_auth_exception from cycode.cli.logger import logger from cycode.cli.models import CliResult -from cycode.cli.printers import ConsolePrinter from cycode.cli.utils.sentry import add_breadcrumb def auth_command(ctx: typer.Context) -> None: """Authenticates your machine.""" add_breadcrumb('auth') + printer = ctx.obj.get('console_printer') if ctx.invoked_subcommand is not None: # if it is a subcommand, do nothing @@ -23,6 +23,6 @@ def auth_command(ctx: typer.Context) -> None: auth_manager.authenticate() result = CliResult(success=True, message='Successfully logged into cycode') - ConsolePrinter(ctx).print_result(result) + printer.print_result(result) except Exception as err: handle_auth_exception(ctx, err) diff --git a/cycode/cli/apps/auth/auth_common.py b/cycode/cli/apps/auth/auth_common.py index fffee388..f6120d94 100644 --- a/cycode/cli/apps/auth/auth_common.py +++ b/cycode/cli/apps/auth/auth_common.py @@ -4,13 +4,14 @@ from cycode.cli.apps.auth.models import AuthInfo from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError -from cycode.cli.printers import ConsolePrinter from cycode.cli.user_settings.credentials_manager import CredentialsManager from cycode.cli.utils.jwt_utils import get_user_and_tenant_ids_from_access_token from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient def get_authorization_info(ctx: Optional[typer.Context] = None) -> Optional[AuthInfo]: + printer = ctx.obj.get('console_printer') + client_id, client_secret = CredentialsManager().get_credentials() if not client_id or not client_secret: return None @@ -24,6 +25,6 @@ def get_authorization_info(ctx: Optional[typer.Context] = None) -> Optional[Auth return AuthInfo(user_id=user_id, tenant_id=tenant_id) except (RequestHttpError, HttpUnauthorizedError): if ctx: - ConsolePrinter(ctx).print_exception() + printer.print_exception() return None diff --git a/cycode/cli/apps/auth/check_command.py b/cycode/cli/apps/auth/check_command.py index cfa57f1c..0a5ea5b3 100644 --- a/cycode/cli/apps/auth/check_command.py +++ b/cycode/cli/apps/auth/check_command.py @@ -2,7 +2,6 @@ from cycode.cli.apps.auth.auth_common import get_authorization_info from cycode.cli.models import CliResult -from cycode.cli.printers import ConsolePrinter from cycode.cli.utils.sentry import add_breadcrumb @@ -10,7 +9,7 @@ def check_command(ctx: typer.Context) -> None: """Checks that your machine is associating the CLI with your Cycode account.""" add_breadcrumb('check') - printer = ConsolePrinter(ctx) + printer = ctx.obj.get('console_printer') auth_info = get_authorization_info(ctx) if auth_info is None: printer.print_result(CliResult(success=False, message='Cycode authentication failed')) diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index 67185dce..a3cae6b5 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -28,7 +28,6 @@ from cycode.cli.files_collector.sca.sca_code_scanner import perform_pre_scan_documents_actions from cycode.cli.files_collector.zip_documents import zip_documents from cycode.cli.models import CliError, Document, DocumentDetections, LocalScanResult -from cycode.cli.printers import ConsolePrinter from cycode.cli.utils import scan_utils from cycode.cli.utils.git_proxy import git_proxy from cycode.cli.utils.path_utils import get_path_by_os @@ -304,10 +303,11 @@ def scan_documents( ) -> None: scan_type = ctx.obj['scan_type'] progress_bar = ctx.obj['progress_bar'] + printer = ctx.obj.get('console_printer') if not documents_to_scan: progress_bar.stop() - ConsolePrinter(ctx).print_error( + printer.print_error( CliError( code='no_relevant_files', message='Error: The scan could not be completed - relevant files to scan are not found. ' @@ -569,7 +569,7 @@ def print_debug_scan_details(scan_details_response: 'ScanDetailsResponse') -> No def print_results( ctx: typer.Context, local_scan_results: List[LocalScanResult], errors: Optional[Dict[str, 'CliError']] = None ) -> None: - printer = ConsolePrinter(ctx) + printer = ctx.obj.get('console_printer') printer.print_scan_results(local_scan_results, errors) diff --git a/cycode/cli/cli_types.py b/cycode/cli/cli_types.py index 2a576ace..b8d4b8ee 100644 --- a/cycode/cli/cli_types.py +++ b/cycode/cli/cli_types.py @@ -10,6 +10,12 @@ class OutputTypeOption(str, Enum): TABLE = 'table' +class ExportTypeOption(str, Enum): + JSON = 'json' + HTML = 'html' + SVG = 'svg' + + class ScanTypeOption(str, Enum): SECRET = consts.SECRET_SCAN_TYPE SCA = consts.SCA_SCAN_TYPE diff --git a/cycode/cli/exceptions/handle_errors.py b/cycode/cli/exceptions/handle_errors.py index b9cb9c80..8d230902 100644 --- a/cycode/cli/exceptions/handle_errors.py +++ b/cycode/cli/exceptions/handle_errors.py @@ -4,14 +4,14 @@ import typer from cycode.cli.models import CliError, CliErrors -from cycode.cli.printers import ConsolePrinter from cycode.cli.utils.sentry import capture_exception def handle_errors( ctx: typer.Context, err: BaseException, cli_errors: CliErrors, *, return_exception: bool = False ) -> Optional['CliError']: - ConsolePrinter(ctx).print_exception(err) + printer = ctx.obj.get('console_printer') + printer.print_exception(err) if type(err) in cli_errors: error = cli_errors[type(err)].enrich(additional_message=str(err)) @@ -22,7 +22,7 @@ def handle_errors( if return_exception: return error - ConsolePrinter(ctx).print_error(error) + printer.print_error(error) return None if isinstance(err, click.ClickException): @@ -34,5 +34,5 @@ def handle_errors( if return_exception: return unknown_error - ConsolePrinter(ctx).print_error(unknown_error) + printer.print_error(unknown_error) raise typer.Exit(1) diff --git a/cycode/cli/printers/console_printer.py b/cycode/cli/printers/console_printer.py index 5ad5dac2..1f6af3ab 100644 --- a/cycode/cli/printers/console_printer.py +++ b/cycode/cli/printers/console_printer.py @@ -1,7 +1,12 @@ +import io from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional, Type import typer +from rich.console import Console +from cycode.cli import consts +from cycode.cli.cli_types import ExportTypeOption +from cycode.cli.console import console, console_err from cycode.cli.exceptions.custom_exceptions import CycodeError from cycode.cli.models import CliError, CliResult from cycode.cli.printers.json_printer import JsonPrinter @@ -27,57 +32,115 @@ class ConsolePrinter: 'rich_sca': ScaTablePrinter, } - def __init__(self, ctx: typer.Context) -> None: + def __init__( + self, + ctx: typer.Context, + console_override: Optional['Console'] = None, + console_err_override: Optional['Console'] = None, + output_type_override: Optional[str] = None, + ) -> None: self.ctx = ctx + self.console = console_override or console + self.console_err = console_err_override or console_err self.scan_type = self.ctx.obj.get('scan_type') - self.output_type = self.ctx.obj.get('output') + self.output_type = output_type_override or self.ctx.obj.get('output') self.aggregation_report_url = self.ctx.obj.get('aggregation_report_url') - self._printer_class = self._AVAILABLE_PRINTERS.get(self.output_type) - if self._printer_class is None: - raise CycodeError(f'"{self.output_type}" output type is not supported.') + self.printer = self._get_scan_printer() - def print_scan_results( - self, - local_scan_results: List['LocalScanResult'], - errors: Optional[Dict[str, 'CliError']] = None, - ) -> None: - printer = self._get_scan_printer() - printer.print_scan_results(local_scan_results, errors) + self.console_record = None + + self.export_type = self.ctx.obj.get('export_type') + self.export_file = self.ctx.obj.get('export_file') + if console_override is None and self.export_type and self.export_file: + self.console_record = ConsolePrinter( + ctx, + console_override=Console(record=True, file=io.StringIO()), + console_err_override=Console(stderr=True, record=True, file=io.StringIO()), + output_type_override='json' if self.export_type == 'json' else self.output_type, + ) def _get_scan_printer(self) -> 'PrinterBase': - printer_class = self._printer_class + printer_class = self._AVAILABLE_PRINTERS.get(self.output_type) composite_printer = self._AVAILABLE_PRINTERS.get(f'{self.output_type}_{self.scan_type}') if composite_printer: printer_class = composite_printer - return printer_class(self.ctx) + if not printer_class: + raise CycodeError(f'"{self.output_type}" output type is not supported.') + + return printer_class(self.ctx, self.console, self.console_err) + + def print_scan_results( + self, + local_scan_results: List['LocalScanResult'], + errors: Optional[Dict[str, 'CliError']] = None, + ) -> None: + if self.console_record: + self.console_record.print_scan_results(local_scan_results, errors) + self.printer.print_scan_results(local_scan_results, errors) def print_result(self, result: CliResult) -> None: - self._printer_class(self.ctx).print_result(result) + if self.console_record: + self.console_record.print_result(result) + self.printer.print_result(result) def print_error(self, error: CliError) -> None: - self._printer_class(self.ctx).print_error(error) + if self.console_record: + self.console_record.print_error(error) + self.printer.print_error(error) def print_exception(self, e: Optional[BaseException] = None, force_print: bool = False) -> None: """Print traceback message in stderr if verbose mode is set.""" if force_print or self.ctx.obj.get('verbose', False): - self._printer_class(self.ctx).print_exception(e) + if self.console_record: + self.console_record.print_exception(e) + self.printer.print_exception(e) + + def export(self) -> None: + if self.console_record is None: + raise CycodeError('Console recording was not enabled. Cannot export.') + + if not self.export_file.suffix: + # resolve file extension based on the export type if not provided in the file name + self.export_file = self.export_file.with_suffix(f'.{self.export_type.lower()}') + + if self.export_type is ExportTypeOption.HTML: + self.console_record.console.save_html(self.export_file) + elif self.export_type is ExportTypeOption.SVG: + self.console_record.console.save_svg(self.export_file, title=consts.APP_NAME) + elif self.export_type is ExportTypeOption.JSON: + with open(self.export_file, 'w', encoding='UTF-8') as f: + self.console_record.console.file.seek(0) + f.write(self.console_record.console.file.read()) + else: + raise CycodeError(f'Export type "{self.export_type}" is not supported.') + + export_format_msg = f'{self.export_type.upper()} format' + if self.export_type in {ExportTypeOption.HTML, ExportTypeOption.SVG}: + export_format_msg += f' with {self.output_type.upper()} output type' + + clickable_path = f'[link=file://{self.export_file}]{self.export_file}[/link]' + self.console.print(f'[b green]Cycode CLI output exported to {clickable_path} in {export_format_msg}[/]') + + @property + def is_recording(self) -> bool: + return self.console_record is not None @property def is_json_printer(self) -> bool: - return self._printer_class == JsonPrinter + return isinstance(self.printer, JsonPrinter) @property def is_table_printer(self) -> bool: - return self._printer_class == TablePrinter + return isinstance(self.printer, TablePrinter) @property def is_text_printer(self) -> bool: - return self._printer_class == TextPrinter + return isinstance(self.printer, TextPrinter) @property def is_rich_printer(self) -> bool: - return self._printer_class == RichPrinter + return isinstance(self.printer, RichPrinter) diff --git a/cycode/cli/printers/json_printer.py b/cycode/cli/printers/json_printer.py index 76b1f7c7..6ad14e22 100644 --- a/cycode/cli/printers/json_printer.py +++ b/cycode/cli/printers/json_printer.py @@ -1,7 +1,6 @@ import json from typing import TYPE_CHECKING, Dict, List, Optional -from cycode.cli.console import console from cycode.cli.models import CliError, CliResult from cycode.cli.printers.printer_base import PrinterBase from cycode.cyclient.models import DetectionSchema @@ -14,12 +13,12 @@ class JsonPrinter(PrinterBase): def print_result(self, result: CliResult) -> None: result = {'result': result.success, 'message': result.message, 'data': result.data} - console.print_json(self.get_data_json(result)) + self.console.print_json(self.get_data_json(result)) def print_error(self, error: CliError) -> None: result = {'error': error.code, 'message': error.message} - console.print_json(self.get_data_json(result)) + self.console.print_json(self.get_data_json(result)) def print_scan_results( self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None @@ -46,7 +45,7 @@ def print_scan_results( # FIXME(MarshalX): we don't care about scan IDs in JSON output due to clumsy JSON root structure inlined_errors = [err._asdict() for err in errors.values()] - console.print_json(self._get_json_scan_result(scan_ids, detections_dict, report_urls, inlined_errors)) + self.console.print_json(self._get_json_scan_result(scan_ids, detections_dict, report_urls, inlined_errors)) def _get_json_scan_result( self, scan_ids: List[str], detections: dict, report_urls: List[str], errors: List[dict] diff --git a/cycode/cli/printers/printer_base.py b/cycode/cli/printers/printer_base.py index 9a86c3d6..f461a446 100644 --- a/cycode/cli/printers/printer_base.py +++ b/cycode/cli/printers/printer_base.py @@ -4,11 +4,12 @@ import typer -from cycode.cli.console import console_err from cycode.cli.models import CliError, CliResult from cycode.cyclient.headers import get_correlation_id if TYPE_CHECKING: + from rich.console import Console + from cycode.cli.models import LocalScanResult @@ -24,8 +25,15 @@ class PrinterBase(ABC): 'Please note that not all results may be available:[/]' ) - def __init__(self, ctx: typer.Context) -> None: + def __init__( + self, + ctx: typer.Context, + console: 'Console', + console_err: 'Console', + ) -> None: self.ctx = ctx + self.console = console + self.console_err = console_err @abstractmethod def print_scan_results( @@ -41,8 +49,7 @@ def print_result(self, result: CliResult) -> None: def print_error(self, error: CliError) -> None: pass - @staticmethod - def print_exception(e: Optional[BaseException] = None) -> None: + def print_exception(self, e: Optional[BaseException] = None) -> None: """We are printing it in stderr so, we don't care about supporting JSON and TABLE outputs. Note: @@ -54,6 +61,6 @@ def print_exception(e: Optional[BaseException] = None) -> None: else RichTraceback.from_exception(*sys.exc_info()) ) rich_traceback.show_locals = False - console_err.print(rich_traceback) + self.console_err.print(rich_traceback) - console_err.print(f'[red]Correlation ID:[/] {get_correlation_id()}') + self.console_err.print(f'[red]Correlation ID:[/] {get_correlation_id()}') diff --git a/cycode/cli/printers/rich_printer.py b/cycode/cli/printers/rich_printer.py index 61cb14ef..6693351a 100644 --- a/cycode/cli/printers/rich_printer.py +++ b/cycode/cli/printers/rich_printer.py @@ -8,7 +8,6 @@ from cycode.cli import consts from cycode.cli.cli_types import SeverityOption -from cycode.cli.console import console from cycode.cli.printers.text_printer import TextPrinter from cycode.cli.printers.utils.code_snippet_syntax import get_code_snippet_syntax from cycode.cli.printers.utils.detection_data import get_detection_title @@ -24,7 +23,7 @@ def print_scan_results( self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): - console.print(self.NO_DETECTIONS_MESSAGE) + self.console.print(self.NO_DETECTIONS_MESSAGE) return current_file = None @@ -44,14 +43,13 @@ def print_scan_results( self.print_report_urls_and_errors(local_scan_results, errors) - @staticmethod - def _print_file_header(file_path: str) -> None: + def _print_file_header(self, file_path: str) -> None: clickable_path = f'[link=file://{file_path}]{file_path}[/link]' file_header = Panel( Text.from_markup(f'[b purple3]:file_folder: File: {clickable_path}[/]', justify='center'), border_style='dim', ) - console.print(file_header) + self.console.print(file_header) def _get_details_table(self, detection: 'Detection') -> Table: details_table = Table(show_header=False, box=None, padding=(0, 1)) @@ -138,4 +136,4 @@ def _print_violation_card( title_align='center', ) - console.print(violation_card_panel) + self.console.print(violation_card_panel) diff --git a/cycode/cli/printers/tables/sca_table_printer.py b/cycode/cli/printers/tables/sca_table_printer.py index 70965be2..e334209c 100644 --- a/cycode/cli/printers/tables/sca_table_printer.py +++ b/cycode/cli/printers/tables/sca_table_printer.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING, Dict, List from cycode.cli.cli_types import SeverityOption -from cycode.cli.console import console from cycode.cli.consts import LICENSE_COMPLIANCE_POLICY_ID, PACKAGE_VULNERABILITY_POLICY_ID from cycode.cli.models import Detection from cycode.cli.printers.tables.table import Table @@ -129,9 +128,8 @@ def _enrich_table_with_values(policy_id: str, table: Table, detection: Detection table.add_cell(CVE_COLUMNS, detection_details.get('vulnerability_id')) table.add_cell(LICENSE_COLUMN, detection_details.get('license')) - @staticmethod - def _print_summary_issues(detections_count: int, title: str) -> None: - console.print(f':no_entry: Found {detections_count} issues of type: [b]{title}[/]') + def _print_summary_issues(self, detections_count: int, title: str) -> None: + self.console.print(f':no_entry: Found {detections_count} issues of type: [b]{title}[/]') @staticmethod def _extract_detections_per_policy_id( diff --git a/cycode/cli/printers/tables/table_printer_base.py b/cycode/cli/printers/tables/table_printer_base.py index 73ab7f88..f36e489d 100644 --- a/cycode/cli/printers/tables/table_printer_base.py +++ b/cycode/cli/printers/tables/table_printer_base.py @@ -3,7 +3,6 @@ import typer -from cycode.cli.console import console from cycode.cli.models import CliError, CliResult from cycode.cli.printers.printer_base import PrinterBase from cycode.cli.printers.text_printer import TextPrinter @@ -14,8 +13,8 @@ class TablePrinterBase(PrinterBase, abc.ABC): - def __init__(self, ctx: typer.Context) -> None: - super().__init__(ctx) + def __init__(self, ctx: typer.Context, *args, **kwargs) -> None: + super().__init__(ctx, *args, **kwargs) self.scan_type: str = ctx.obj.get('scan_type') self.show_secret: bool = ctx.obj.get('show_secret', False) @@ -29,7 +28,7 @@ def print_scan_results( self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): - console.print(self.NO_DETECTIONS_MESSAGE) + self.console.print(self.NO_DETECTIONS_MESSAGE) return self._print_results(local_scan_results) @@ -37,9 +36,9 @@ def print_scan_results( if not errors: return - console.print(self.FAILED_SCAN_MESSAGE) + self.console.print(self.FAILED_SCAN_MESSAGE) for scan_id, error in errors.items(): - console.print(f'- {scan_id}: ', end='') + self.console.print(f'- {scan_id}: ', end='') self.print_error(error) def _is_git_repository(self) -> bool: @@ -49,13 +48,12 @@ def _is_git_repository(self) -> bool: def _print_results(self, local_scan_results: List['LocalScanResult']) -> None: raise NotImplementedError - @staticmethod - def _print_table(table: 'Table') -> None: + def _print_table(self, table: 'Table') -> None: if table.get_rows(): - console.print(table.get_table()) + self.console.print(table.get_table()) - @staticmethod def _print_report_urls( + self, local_scan_results: List['LocalScanResult'], aggregation_report_url: Optional[str] = None, ) -> None: @@ -63,9 +61,9 @@ def _print_report_urls( if not report_urls and not aggregation_report_url: return if aggregation_report_url: - console.print(f'Report URL: {aggregation_report_url}') + self.console.print(f'Report URL: {aggregation_report_url}') return - console.print('Report URLs:') + self.console.print('Report URLs:') for report_url in report_urls: - console.print(f'- {report_url}') + self.console.print(f'- {report_url}') diff --git a/cycode/cli/printers/text_printer.py b/cycode/cli/printers/text_printer.py index b1eeb38e..f4dcf19a 100644 --- a/cycode/cli/printers/text_printer.py +++ b/cycode/cli/printers/text_printer.py @@ -5,7 +5,6 @@ from rich.markup import escape from cycode.cli.cli_types import SeverityOption -from cycode.cli.console import console from cycode.cli.models import CliError, CliResult, Document from cycode.cli.printers.printer_base import PrinterBase from cycode.cli.printers.utils.code_snippet_syntax import get_code_snippet_syntax @@ -17,8 +16,8 @@ class TextPrinter(PrinterBase): - def __init__(self, ctx: typer.Context) -> None: - super().__init__(ctx) + def __init__(self, ctx: typer.Context, *args, **kwargs) -> None: + super().__init__(ctx, *args, **kwargs) self.scan_type = ctx.obj.get('scan_type') self.command_scan_type: str = ctx.info_name self.show_secret: bool = ctx.obj.get('show_secret', False) @@ -28,23 +27,23 @@ def print_result(self, result: CliResult) -> None: if not result.success: color = 'red' - console.print(result.message, style=color) + self.console.print(result.message, style=color) if not result.data: return - console.print('\nAdditional data:', style=color) + self.console.print('\nAdditional data:', style=color) for name, value in result.data.items(): - console.print(f'- {name}: {value}', style=color) + self.console.print(f'- {name}: {value}', style=color) def print_error(self, error: CliError) -> None: - console.print(f'[red]Error: {error.message}[/]', highlight=False) + self.console.print(f'[red]Error: {error.message}[/]', highlight=False) def print_scan_results( self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): - console.print(self.NO_DETECTIONS_MESSAGE) + self.console.print(self.NO_DETECTIONS_MESSAGE) return detections, _ = sort_and_group_detections_from_scan_result(local_scan_results) @@ -58,9 +57,8 @@ def __print_document_detection(self, document: 'Document', detection: 'Detection self.__print_detection_code_segment(detection, document) self._print_new_line() - @staticmethod - def _print_new_line() -> None: - console.line() + def _print_new_line(self) -> None: + self.console.line() def __print_detection_summary(self, detection: 'Detection', document_path: str) -> None: title = get_detection_title(self.scan_type, detection) @@ -74,14 +72,14 @@ def __print_detection_summary(self, detection: 'Detection', document_path: str) detection_commit_id = detection.detection_details.get('commit_id') detection_commit_id_message = f'\nCommit SHA: {detection_commit_id}' if detection_commit_id else '' - console.print( + self.console.print( f'{severity_icon}', severity, f'violation: [b bright_red]{title}[/]{detection_commit_id_message}\n{clickable_document_path}:', ) def __print_detection_code_segment(self, detection: 'Detection', document: Document) -> None: - console.print( + self.console.print( get_code_snippet_syntax( self.scan_type, self.command_scan_type, @@ -100,19 +98,18 @@ def print_report_urls_and_errors( if not errors: return - console.print(self.FAILED_SCAN_MESSAGE) + self.console.print(self.FAILED_SCAN_MESSAGE) for scan_id, error in errors.items(): - console.print(f'- {scan_id}: ', end='') + self.console.print(f'- {scan_id}: ', end='') self.print_error(error) - @staticmethod - def print_report_urls(report_urls: List[str], aggregation_report_url: Optional[str] = None) -> None: + def print_report_urls(self, report_urls: List[str], aggregation_report_url: Optional[str] = None) -> None: if not report_urls and not aggregation_report_url: return if aggregation_report_url: - console.print(f'Report URL: {aggregation_report_url}') + self.console.print(f'Report URL: {aggregation_report_url}') return - console.print('Report URLs:') + self.console.print('Report URLs:') for report_url in report_urls: - console.print(f'- {report_url}') + self.console.print(f'- {report_url}') diff --git a/tests/cli/exceptions/test_handle_scan_errors.py b/tests/cli/exceptions/test_handle_scan_errors.py index a6b2a9ec..abd297db 100644 --- a/tests/cli/exceptions/test_handle_scan_errors.py +++ b/tests/cli/exceptions/test_handle_scan_errors.py @@ -10,6 +10,7 @@ from cycode.cli.console import console_err from cycode.cli.exceptions import custom_exceptions from cycode.cli.exceptions.handle_scan_errors import handle_scan_exception +from cycode.cli.printers import ConsolePrinter from cycode.cli.utils.git_proxy import git_proxy if TYPE_CHECKING: @@ -18,7 +19,9 @@ @pytest.fixture() def ctx() -> typer.Context: - return typer.Context(click.Command('path'), obj={'verbose': False, 'output': OutputTypeOption.TEXT}) + ctx = typer.Context(click.Command('path'), obj={'verbose': False, 'output': OutputTypeOption.TEXT}) + ctx.obj['console_printer'] = ConsolePrinter(ctx) + return ctx @pytest.mark.parametrize( @@ -60,6 +63,7 @@ def test_handle_exception_click_error(ctx: typer.Context) -> None: def test_handle_exception_verbose(monkeypatch: 'MonkeyPatch') -> None: ctx = typer.Context(click.Command('path'), obj={'verbose': True, 'output': OutputTypeOption.TEXT}) + ctx.obj['console_printer'] = ConsolePrinter(ctx) error_text = 'test'