diff --git a/cycode/cli/app.py b/cycode/cli/app.py index 5c83be9e..507c03c8 100644 --- a/cycode/cli/app.py +++ b/cycode/cli/app.py @@ -3,6 +3,7 @@ from typing import Annotated, Optional import typer +from typer import rich_utils from typer.completion import install_callback, show_callback from cycode import __version__ @@ -18,11 +19,18 @@ from cycode.cyclient.models import UserAgentOptionScheme from cycode.logger import set_logging_level +# By default, it uses dim style which is hard to read with the combination of color from RICH_HELP +rich_utils.STYLE_ERRORS_SUGGESTION = 'bold' +# By default, it uses blue color which is too dark for some terminals +rich_utils.RICH_HELP = "Try [cyan]'{command_path} {help_option}'[/] for help." + + app = typer.Typer( pretty_exceptions_show_locals=False, pretty_exceptions_short=True, context_settings=CLI_CONTEXT_SETTINGS, rich_markup_mode='rich', + no_args_is_help=True, add_completion=False, # we add it manually to control the rich help panel ) diff --git a/cycode/cli/apps/ai_remediation/__init__.py b/cycode/cli/apps/ai_remediation/__init__.py index 6b5a3013..0f017cf7 100644 --- a/cycode/cli/apps/ai_remediation/__init__.py +++ b/cycode/cli/apps/ai_remediation/__init__.py @@ -2,7 +2,7 @@ from cycode.cli.apps.ai_remediation.ai_remediation_command import ai_remediation_command -app = typer.Typer() +app = typer.Typer(no_args_is_help=True) app.command(name='ai-remediation', short_help='Get AI remediation (INTERNAL).', hidden=True)(ai_remediation_command) # backward compatibility diff --git a/cycode/cli/apps/auth/__init__.py b/cycode/cli/apps/auth/__init__.py index 82e71fbc..951a9f1f 100644 --- a/cycode/cli/apps/auth/__init__.py +++ b/cycode/cli/apps/auth/__init__.py @@ -6,6 +6,7 @@ app = typer.Typer( name='auth', help='Authenticate your machine to associate the CLI with your Cycode account.', + no_args_is_help=True, ) app.callback(invoke_without_command=True)(auth_command) app.command(name='check')(check_command) diff --git a/cycode/cli/apps/configure/__init__.py b/cycode/cli/apps/configure/__init__.py index 815874d1..039c6f2e 100644 --- a/cycode/cli/apps/configure/__init__.py +++ b/cycode/cli/apps/configure/__init__.py @@ -2,7 +2,7 @@ from cycode.cli.apps.configure.configure_command import configure_command -app = typer.Typer() +app = typer.Typer(no_args_is_help=True) app.command(name='configure', short_help='Initial command to configure your CLI client authentication.')( configure_command ) diff --git a/cycode/cli/apps/ignore/__init__.py b/cycode/cli/apps/ignore/__init__.py index 3c51d38a..e6573b69 100644 --- a/cycode/cli/apps/ignore/__init__.py +++ b/cycode/cli/apps/ignore/__init__.py @@ -2,5 +2,5 @@ from cycode.cli.apps.ignore.ignore_command import ignore_command -app = typer.Typer() +app = typer.Typer(no_args_is_help=True) app.command(name='ignore', short_help='Ignores a specific value, path or rule ID.')(ignore_command) diff --git a/cycode/cli/apps/report/__init__.py b/cycode/cli/apps/report/__init__.py index f71532c8..40cc696a 100644 --- a/cycode/cli/apps/report/__init__.py +++ b/cycode/cli/apps/report/__init__.py @@ -3,6 +3,6 @@ from cycode.cli.apps.report import sbom from cycode.cli.apps.report.report_command import report_command -app = typer.Typer(name='report') +app = typer.Typer(name='report', no_args_is_help=True) app.callback(short_help='Generate report. You`ll need to specify which report type to perform.')(report_command) app.add_typer(sbom.app) diff --git a/cycode/cli/apps/scan/__init__.py b/cycode/cli/apps/scan/__init__.py index 07c15978..136e7bef 100644 --- a/cycode/cli/apps/scan/__init__.py +++ b/cycode/cli/apps/scan/__init__.py @@ -7,7 +7,7 @@ from cycode.cli.apps.scan.repository.repository_command import repository_command from cycode.cli.apps.scan.scan_command import scan_command, scan_command_result_callback -app = typer.Typer(name='scan') +app = typer.Typer(name='scan', no_args_is_help=True) app.callback( short_help='Scan the content for Secrets, IaC, SCA, and SAST violations.', diff --git a/cycode/cli/apps/status/__init__.py b/cycode/cli/apps/status/__init__.py index f01e3b30..1161b2e6 100644 --- a/cycode/cli/apps/status/__init__.py +++ b/cycode/cli/apps/status/__init__.py @@ -3,6 +3,6 @@ from cycode.cli.apps.status.status_command import status_command from cycode.cli.apps.status.version_command import version_command -app = typer.Typer() +app = typer.Typer(no_args_is_help=True) app.command(name='status', short_help='Show the CLI status and exit.')(status_command) app.command(name='version', hidden=True, short_help='Alias to status command.')(version_command) diff --git a/cycode/cli/cli_types.py b/cycode/cli/cli_types.py index b8d4b8ee..9b792a01 100644 --- a/cycode/cli/cli_types.py +++ b/cycode/cli/cli_types.py @@ -94,6 +94,6 @@ def __rich__(self) -> str: SeverityOption.INFO.value: ':blue_circle:', SeverityOption.LOW.value: ':yellow_circle:', SeverityOption.MEDIUM.value: ':orange_circle:', - SeverityOption.HIGH.value: ':heavy_large_circle:', - SeverityOption.CRITICAL.value: ':red_circle:', + SeverityOption.HIGH.value: ':red_circle:', + SeverityOption.CRITICAL.value: ':exclamation_mark:', # double_exclamation_mark is not red } diff --git a/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py b/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py index 68175d88..672ee0db 100644 --- a/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +++ b/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py @@ -37,5 +37,6 @@ def get_lock_file_name(self) -> str: def verify_restore_file_already_exist(self, restore_file_path: str) -> bool: return os.path.isfile(restore_file_path) - def prepare_manifest_file_path_for_command(self, manifest_file_path: str) -> str: + @staticmethod + def prepare_manifest_file_path_for_command(manifest_file_path: str) -> str: return manifest_file_path.replace(os.sep + NPM_MANIFEST_FILE_NAME, '') diff --git a/cycode/cli/files_collector/sca/sca_code_scanner.py b/cycode/cli/files_collector/sca/sca_code_scanner.py index fc8c3809..88626c9c 100644 --- a/cycode/cli/files_collector/sca/sca_code_scanner.py +++ b/cycode/cli/files_collector/sca/sca_code_scanner.py @@ -124,14 +124,15 @@ def try_restore_dependencies( def add_dependencies_tree_document( ctx: typer.Context, documents_to_scan: List[Document], is_git_diff: bool = False ) -> None: - documents_to_add: Dict[str, Document] = {} + documents_to_add: Dict[str, Document] = {document.path: document for document in documents_to_scan} restore_dependencies_list = restore_handlers(ctx, is_git_diff) for restore_dependencies in restore_dependencies_list: for document in documents_to_scan: try_restore_dependencies(ctx, documents_to_add, restore_dependencies, document) - documents_to_scan.extend(list(documents_to_add.values())) + # mutate original list using slice assignment + documents_to_scan[:] = list(documents_to_add.values()) def restore_handlers(ctx: typer.Context, is_git_diff: bool) -> List[BaseRestoreDependencies]: diff --git a/cycode/cli/printers/console_printer.py b/cycode/cli/printers/console_printer.py index 1f6af3ab..00eb38cf 100644 --- a/cycode/cli/printers/console_printer.py +++ b/cycode/cli/printers/console_printer.py @@ -29,7 +29,6 @@ class ConsolePrinter: # overrides 'table_sca': ScaTablePrinter, 'text_sca': ScaTablePrinter, - 'rich_sca': ScaTablePrinter, } def __init__( @@ -42,12 +41,7 @@ def __init__( 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 = output_type_override or self.ctx.obj.get('output') - self.aggregation_report_url = self.ctx.obj.get('aggregation_report_url') - - self.printer = self._get_scan_printer() self.console_record = None @@ -61,7 +55,16 @@ def __init__( output_type_override='json' if self.export_type == 'json' else self.output_type, ) - def _get_scan_printer(self) -> 'PrinterBase': + @property + def scan_type(self) -> str: + return self.ctx.obj.get('scan_type') + + @property + def aggregation_report_url(self) -> str: + return self.ctx.obj.get('aggregation_report_url') + + @property + def printer(self) -> 'PrinterBase': printer_class = self._AVAILABLE_PRINTERS.get(self.output_type) composite_printer = self._AVAILABLE_PRINTERS.get(f'{self.output_type}_{self.scan_type}') diff --git a/cycode/cli/printers/printer_base.py b/cycode/cli/printers/printer_base.py index f461a446..23ba7384 100644 --- a/cycode/cli/printers/printer_base.py +++ b/cycode/cli/printers/printer_base.py @@ -1,9 +1,11 @@ import sys from abc import ABC, abstractmethod +from collections import defaultdict from typing import TYPE_CHECKING, Dict, List, Optional import typer +from cycode.cli.cli_types import SeverityOption from cycode.cli.models import CliError, CliResult from cycode.cyclient.headers import get_correlation_id @@ -35,6 +37,18 @@ def __init__( self.console = console self.console_err = console_err + @property + def scan_type(self) -> str: + return self.ctx.obj.get('scan_type') + + @property + def command_scan_type(self) -> str: + return self.ctx.info_name + + @property + def show_secret(self) -> bool: + return self.ctx.obj.get('show_secret', False) + @abstractmethod def print_scan_results( self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None @@ -64,3 +78,34 @@ def print_exception(self, e: Optional[BaseException] = None) -> None: self.console_err.print(rich_traceback) self.console_err.print(f'[red]Correlation ID:[/] {get_correlation_id()}') + + def print_scan_results_summary(self, local_scan_results: List['LocalScanResult']) -> None: + """Print a summary of scan results based on severity levels. + + Args: + local_scan_results (List['LocalScanResult']): A list of local scan results containing detections. + + The summary includes the count of detections for each severity level + and is displayed in the console in a formatted string. + """ + + detections_count = 0 + severity_counts = defaultdict(int) + for local_scan_result in local_scan_results: + for document_detections in local_scan_result.document_detections: + for detection in document_detections.detections: + if detection.severity: + detections_count += 1 + severity_counts[SeverityOption(detection.severity)] += 1 + + self.console.print(f'[bold]Cycode found {detections_count} violations[/]', end=': ') + + # Example of string: CRITICAL - 6 | HIGH - 0 | MEDIUM - 14 | LOW - 0 | INFO - 0 + for index, severity in enumerate(reversed(SeverityOption), start=1): + end = ' | ' + if index == len(SeverityOption): + end = '\n' + + self.console.print( + SeverityOption.get_member_emoji(severity), severity, '-', severity_counts[severity], end=end + ) diff --git a/cycode/cli/printers/rich_printer.py b/cycode/cli/printers/rich_printer.py index 6693351a..3401b8f5 100644 --- a/cycode/cli/printers/rich_printer.py +++ b/cycode/cli/printers/rich_printer.py @@ -1,4 +1,3 @@ -from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional from rich.console import Group @@ -10,7 +9,11 @@ from cycode.cli.cli_types import SeverityOption 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 +from cycode.cli.printers.utils.detection_data import ( + get_detection_clickable_cwe_cve, + get_detection_file_path, + get_detection_title, +) from cycode.cli.printers.utils.detection_ordering.common_ordering import sort_and_group_detections_from_scan_result from cycode.cli.printers.utils.rich_helpers import get_columns_in_1_to_3_ratio, get_markdown_panel, get_panel @@ -19,6 +22,8 @@ class RichPrinter(TextPrinter): + MAX_PATH_LENGTH = 60 + def print_scan_results( self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None ) -> None: @@ -26,14 +31,9 @@ def print_scan_results( self.console.print(self.NO_DETECTIONS_MESSAGE) return - current_file = None detections, _ = sort_and_group_detections_from_scan_result(local_scan_results) detections_count = len(detections) for detection_number, (detection, document) in enumerate(detections, start=1): - if current_file != document.path: - current_file = document.path - self._print_file_header(current_file) - self._print_violation_card( document, detection, @@ -41,16 +41,9 @@ def print_scan_results( detections_count, ) + self.print_scan_results_summary(local_scan_results) self.print_report_urls_and_errors(local_scan_results, errors) - 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', - ) - self.console.print(file_header) - def _get_details_table(self, detection: 'Detection') -> Table: details_table = Table(show_header=False, box=None, padding=(0, 1)) @@ -62,15 +55,32 @@ def _get_details_table(self, detection: 'Detection') -> Table: details_table.add_row('Severity', f'{severity_icon} {SeverityOption(severity).__rich__()}') detection_details = detection.detection_details - path = Path(detection_details.get('file_name', '')) - details_table.add_row('In file', path.name) # it is name already except for IaC :) - # we do not allow using rich output with SCA; SCA designed to be used with table output - if self.scan_type == consts.IAC_SCAN_TYPE: - details_table.add_row('IaC Provider', detection_details.get('infra_provider')) - elif self.scan_type == consts.SECRET_SCAN_TYPE: + path = str(get_detection_file_path(self.scan_type, detection)) + shorten_path = f'...{path[-self.MAX_PATH_LENGTH:]}' if len(path) > self.MAX_PATH_LENGTH else path + details_table.add_row('In file', f'[link=file://{path}]{shorten_path}[/]') + + if self.scan_type == consts.SECRET_SCAN_TYPE: details_table.add_row('Secret SHA', detection_details.get('sha512')) + elif self.scan_type == consts.SCA_SCAN_TYPE: + details_table.add_row('CVEs', get_detection_clickable_cwe_cve(self.scan_type, detection)) + details_table.add_row('Package', detection_details.get('package_name')) + details_table.add_row('Version', detection_details.get('package_version')) + + is_package_vulnerability = 'alert' in detection_details + if is_package_vulnerability: + details_table.add_row( + 'First patched version', detection_details['alert'].get('first_patched_version', 'Not fixed') + ) + + details_table.add_row('Dependency path', detection_details.get('dependency_paths', 'N/A')) + + if not is_package_vulnerability: + details_table.add_row('License', detection_details.get('license')) + elif self.scan_type == consts.IAC_SCAN_TYPE: + details_table.add_row('IaC Provider', detection_details.get('infra_provider')) elif self.scan_type == consts.SAST_SCAN_TYPE: + details_table.add_row('CWE', get_detection_clickable_cwe_cve(self.scan_type, detection)) details_table.add_row('Subcategory', detection_details.get('category')) details_table.add_row('Language', ', '.join(detection_details.get('languages', []))) @@ -105,12 +115,17 @@ def _print_violation_card( title=':computer: Code Snippet', ) - guidelines_panel = None - guidelines = detection.detection_details.get('remediation_guidelines') - if guidelines: - guidelines_panel = get_markdown_panel( - guidelines, - title=':clipboard: Cycode Guidelines', + is_sca_package_vulnerability = self.scan_type == consts.SCA_SCAN_TYPE and 'alert' in detection.detection_details + if is_sca_package_vulnerability: + summary = detection.detection_details['alert'].get('description') + else: + summary = detection.detection_details.get('description') or detection.message + + summary_panel = None + if summary: + summary_panel = get_markdown_panel( + summary, + title=':memo: Summary', ) custom_guidelines_panel = None @@ -124,8 +139,8 @@ def _print_violation_card( navigation = Text(f'Violation {detection_number} of {detections_count}', style='dim', justify='right') renderables = [navigation, get_columns_in_1_to_3_ratio(details_panel, code_snippet_panel)] - if guidelines_panel: - renderables.append(guidelines_panel) + if summary_panel: + renderables.append(summary_panel) if custom_guidelines_panel: renderables.append(custom_guidelines_panel) diff --git a/cycode/cli/printers/tables/sca_table_printer.py b/cycode/cli/printers/tables/sca_table_printer.py index e334209c..74ac2832 100644 --- a/cycode/cli/printers/tables/sca_table_printer.py +++ b/cycode/cli/printers/tables/sca_table_printer.py @@ -45,6 +45,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None: self._print_summary_issues(len(detections), self._get_title(policy_id)) self._print_table(table) + self.print_scan_results_summary(local_scan_results) self._print_report_urls(local_scan_results, aggregation_report_url) @staticmethod @@ -129,7 +130,7 @@ def _enrich_table_with_values(policy_id: str, table: Table, detection: Detection table.add_cell(LICENSE_COLUMN, detection_details.get('license')) 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}[/]') + self.console.print(f'[bold]Cycode found {detections_count} violations of type: [cyan]{title}[/]') @staticmethod def _extract_detections_per_policy_id( diff --git a/cycode/cli/printers/tables/table_printer.py b/cycode/cli/printers/tables/table_printer.py index e36b1b01..4f821c7f 100644 --- a/cycode/cli/printers/tables/table_printer.py +++ b/cycode/cli/printers/tables/table_printer.py @@ -37,6 +37,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None: table.set_group_separator_indexes(group_separator_indexes) self._print_table(table) + self.print_scan_results_summary(local_scan_results) self._print_report_urls(local_scan_results, self.ctx.obj.get('aggregation_report_url')) def _get_table(self) -> Table: diff --git a/cycode/cli/printers/tables/table_printer_base.py b/cycode/cli/printers/tables/table_printer_base.py index f36e489d..5d2aaa73 100644 --- a/cycode/cli/printers/tables/table_printer_base.py +++ b/cycode/cli/printers/tables/table_printer_base.py @@ -1,8 +1,6 @@ import abc from typing import TYPE_CHECKING, Dict, List, Optional -import typer - from cycode.cli.models import CliError, CliResult from cycode.cli.printers.printer_base import PrinterBase from cycode.cli.printers.text_printer import TextPrinter @@ -13,16 +11,11 @@ class TablePrinterBase(PrinterBase, abc.ABC): - 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) - def print_result(self, result: CliResult) -> None: - TextPrinter(self.ctx).print_result(result) + TextPrinter(self.ctx, self.console, self.console_err).print_result(result) def print_error(self, error: CliError) -> None: - TextPrinter(self.ctx).print_error(error) + TextPrinter(self.ctx, self.console, self.console_err).print_error(error) def print_scan_results( self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None diff --git a/cycode/cli/printers/text_printer.py b/cycode/cli/printers/text_printer.py index f4dcf19a..6eb4b78b 100644 --- a/cycode/cli/printers/text_printer.py +++ b/cycode/cli/printers/text_printer.py @@ -1,13 +1,9 @@ -import urllib.parse from typing import TYPE_CHECKING, Dict, List, Optional -import typer -from rich.markup import escape - from cycode.cli.cli_types import SeverityOption 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 +from cycode.cli.printers.utils.code_snippet_syntax import get_code_snippet_syntax, get_detection_line from cycode.cli.printers.utils.detection_data import get_detection_title from cycode.cli.printers.utils.detection_ordering.common_ordering import sort_and_group_detections_from_scan_result @@ -16,12 +12,6 @@ class TextPrinter(PrinterBase): - 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) - def print_result(self, result: CliResult) -> None: color = 'default' if not result.success: @@ -50,6 +40,7 @@ def print_scan_results( for detection, document in detections: self.__print_document_detection(document, detection) + self.print_scan_results_summary(local_scan_results) self.print_report_urls_and_errors(local_scan_results, errors) def __print_document_detection(self, document: 'Document', detection: 'Detection') -> None: @@ -66,16 +57,17 @@ def __print_detection_summary(self, detection: 'Detection', document_path: str) severity = SeverityOption(detection.severity) if detection.severity else 'N/A' severity_icon = SeverityOption.get_member_emoji(detection.severity) if detection.severity else '' - escaped_document_path = escape(urllib.parse.quote(document_path)) - clickable_document_path = f'[link file://{escaped_document_path}]{document_path}' + line_no = get_detection_line(self.scan_type, detection) + 1 + clickable_document_path = f'[u]{document_path}:{line_no}[/]' detection_commit_id = detection.detection_details.get('commit_id') detection_commit_id_message = f'\nCommit SHA: {detection_commit_id}' if detection_commit_id else '' self.console.print( - f'{severity_icon}', + severity_icon, severity, - f'violation: [b bright_red]{title}[/]{detection_commit_id_message}\n{clickable_document_path}:', + f'violation: [b bright_red]{title}[/]{detection_commit_id_message}\n' + f'[dodger_blue1]File: {clickable_document_path}[/]', ) def __print_detection_code_segment(self, detection: 'Detection', document: Document) -> None: diff --git a/cycode/cli/printers/utils/code_snippet_syntax.py b/cycode/cli/printers/utils/code_snippet_syntax.py index c3c9f59b..aae33872 100644 --- a/cycode/cli/printers/utils/code_snippet_syntax.py +++ b/cycode/cli/printers/utils/code_snippet_syntax.py @@ -17,7 +17,7 @@ def _get_code_segment_start_line(detection_line: int, lines_to_display: int) -> return 0 if start_line < 0 else start_line -def _get_detection_line(scan_type: str, detection: 'Detection') -> int: +def get_detection_line(scan_type: str, detection: 'Detection') -> int: return ( detection.detection_details.get('line', -1) if scan_type == consts.SECRET_SCAN_TYPE @@ -29,7 +29,7 @@ def _get_code_snippet_syntax_from_file( scan_type: str, detection: 'Detection', document: 'Document', lines_to_display: int, obfuscate: bool ) -> Syntax: detection_details = detection.detection_details - detection_line = _get_detection_line(scan_type, detection) + detection_line = get_detection_line(scan_type, detection) start_line_index = _get_code_segment_start_line(detection_line, lines_to_display) detection_position = get_position_in_line(document.content, detection_details.get('start_position', -1)) violation_length = detection_details.get('length', -1) @@ -69,7 +69,7 @@ def _get_code_snippet_syntax_from_git_diff( scan_type: str, detection: 'Detection', document: 'Document', obfuscate: bool ) -> Syntax: detection_details = detection.detection_details - detection_line = _get_detection_line(scan_type, detection) + detection_line = get_detection_line(scan_type, detection) detection_position = detection_details.get('start_position', -1) violation_length = detection_details.get('length', -1) diff --git a/cycode/cli/printers/utils/detection_data.py b/cycode/cli/printers/utils/detection_data.py index 66171226..358b4c63 100644 --- a/cycode/cli/printers/utils/detection_data.py +++ b/cycode/cli/printers/utils/detection_data.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, Optional from cycode.cli import consts @@ -6,6 +7,63 @@ from cycode.cyclient.models import Detection +def get_cwe_cve_link(cwe_cve: Optional[str]) -> Optional[str]: + if not cwe_cve: + return None + + if cwe_cve.startswith('GHSA'): + return f'https://github.com/advisories/{cwe_cve}' + + if cwe_cve.startswith('CWE'): + # string example: 'CWE-532: Insertion of Sensitive Information into Log File' + parts = cwe_cve.split('-') + if len(parts) < 1: + return None + + number = '' + for char in parts[1]: + if char.isdigit(): + number += char + else: + break + + return f'https://cwe.mitre.org/data/definitions/{number}' + + if cwe_cve.startswith('CVE'): + return f'https://cve.mitre.org/cgi-bin/cvename.cgi?name={cwe_cve}' + + return None + + +def get_detection_clickable_cwe_cve(scan_type: str, detection: 'Detection') -> str: + def link(url: str, name: str) -> str: + return f'[link={url}]{name}[/]' + + if scan_type == consts.SCA_SCAN_TYPE: + cve = detection.detection_details.get('vulnerability_id') + return link(get_cwe_cve_link(cve), cve) if cve else '' + if scan_type == consts.SAST_SCAN_TYPE: + renderables = [] + for cwe in detection.detection_details.get('cwe', []): + cwe and renderables.append(link(get_cwe_cve_link(cwe), cwe)) + return ', '.join(renderables) + + return '' + + +def get_detection_cwe_cve(scan_type: str, detection: 'Detection') -> Optional[str]: + if scan_type == consts.SCA_SCAN_TYPE: + return detection.detection_details.get('vulnerability_id') + if scan_type == consts.SAST_SCAN_TYPE: + cwes = detection.detection_details.get('cwe') # actually it is List[str] + if not cwes: + return None + + return ' | '.join(cwes) + + return None + + def get_detection_title(scan_type: str, detection: 'Detection') -> str: title = detection.message if scan_type == consts.SAST_SCAN_TYPE: @@ -13,4 +71,18 @@ def get_detection_title(scan_type: str, detection: 'Detection') -> str: elif scan_type == consts.SECRET_SCAN_TYPE: title = f'Hardcoded {detection.type} is used' - return title + is_sca_package_vulnerability = scan_type == consts.SCA_SCAN_TYPE and 'alert' in detection.detection_details + if is_sca_package_vulnerability: + title = detection.detection_details['alert'].get('summary', 'N/A') + + cwe_cve = get_detection_cwe_cve(scan_type, detection) + return f'[{cwe_cve}] {title}' if cwe_cve else title + + +def get_detection_file_path(scan_type: str, detection: 'Detection') -> Path: + if scan_type == consts.SECRET_SCAN_TYPE: + folder_path = detection.detection_details.get('file_path', '') + file_name = detection.detection_details.get('file_name', '') + return Path.joinpath(Path(folder_path), Path(file_name)) + + return Path(detection.detection_details.get('file_name', '')) diff --git a/cycode/cli/printers/utils/detection_ordering/common_ordering.py b/cycode/cli/printers/utils/detection_ordering/common_ordering.py index 531cbc4c..d93b858e 100644 --- a/cycode/cli/printers/utils/detection_ordering/common_ordering.py +++ b/cycode/cli/printers/utils/detection_ordering/common_ordering.py @@ -1,4 +1,3 @@ -from collections import defaultdict from typing import TYPE_CHECKING, List, Set, Tuple from cycode.cli.cli_types import SeverityOption @@ -37,22 +36,14 @@ def _sort_detections_by_file_path( def sort_and_group_detections( detections_with_documents: List[Tuple['Detection', 'Document']], ) -> GroupedDetections: - """Sort detections by severity and group by file name.""" - detections = [] + """Sort detections by severity. We do not have groping here (don't find the best one yet).""" group_separator_indexes = set() # we sort detections by file path to make persist output order - sorted_detections = _sort_detections_by_file_path(detections_with_documents) + sorted_by_path_detections = _sort_detections_by_file_path(detections_with_documents) + sorted_by_severity = _sort_detections_by_severity(sorted_by_path_detections) - grouped_by_file_path = defaultdict(list) - for detection, document in sorted_detections: - grouped_by_file_path[document.path].append((detection, document)) - - for file_path_group in grouped_by_file_path.values(): - group_separator_indexes.add(len(detections) - 1) # indexing starts from 0 - detections.extend(_sort_detections_by_severity(file_path_group)) - - return detections, group_separator_indexes + return sorted_by_severity, group_separator_indexes def sort_and_group_detections_from_scan_result(local_scan_results: List['LocalScanResult']) -> GroupedDetections: