|
| 1 | +from pathlib import Path |
| 2 | +from typing import TYPE_CHECKING, Dict, List, Optional |
| 3 | + |
| 4 | +from rich.console import Group |
| 5 | +from rich.panel import Panel |
| 6 | +from rich.table import Table |
| 7 | +from rich.text import Text |
| 8 | + |
| 9 | +from cycode.cli import consts |
| 10 | +from cycode.cli.cli_types import SeverityOption |
| 11 | +from cycode.cli.console import console |
| 12 | +from cycode.cli.printers.text_printer import TextPrinter |
| 13 | +from cycode.cli.printers.utils.code_snippet_syntax import get_code_snippet_syntax |
| 14 | +from cycode.cli.printers.utils.detection_data import get_detection_title |
| 15 | +from cycode.cli.printers.utils.detection_ordering.common_ordering import sort_and_group_detections_from_scan_result |
| 16 | +from cycode.cli.printers.utils.rich_helpers import get_columns_in_1_to_3_ratio, get_markdown_panel, get_panel |
| 17 | + |
| 18 | +if TYPE_CHECKING: |
| 19 | + from cycode.cli.models import CliError, Detection, Document, LocalScanResult |
| 20 | + |
| 21 | + |
| 22 | +class RichPrinter(TextPrinter): |
| 23 | + def print_scan_results( |
| 24 | + self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None |
| 25 | + ) -> None: |
| 26 | + if not errors and all(result.issue_detected == 0 for result in local_scan_results): |
| 27 | + console.print(self.NO_DETECTIONS_MESSAGE) |
| 28 | + return |
| 29 | + |
| 30 | + current_file = None |
| 31 | + detections, _ = sort_and_group_detections_from_scan_result(local_scan_results) |
| 32 | + detections_count = len(detections) |
| 33 | + for detection_number, (detection, document) in enumerate(detections, start=1): |
| 34 | + if current_file != document.path: |
| 35 | + current_file = document.path |
| 36 | + self._print_file_header(current_file) |
| 37 | + |
| 38 | + self._print_violation_card( |
| 39 | + document, |
| 40 | + detection, |
| 41 | + detection_number, |
| 42 | + detections_count, |
| 43 | + ) |
| 44 | + |
| 45 | + self.print_report_urls_and_errors(local_scan_results, errors) |
| 46 | + |
| 47 | + @staticmethod |
| 48 | + def _print_file_header(file_path: str) -> None: |
| 49 | + clickable_path = f'[link=file://{file_path}]{file_path}[/link]' |
| 50 | + file_header = Panel( |
| 51 | + Text.from_markup(f'[b purple3]:file_folder: File: {clickable_path}[/]', justify='center'), |
| 52 | + border_style='dim', |
| 53 | + ) |
| 54 | + console.print(file_header) |
| 55 | + |
| 56 | + def _get_details_table(self, detection: 'Detection') -> Table: |
| 57 | + details_table = Table(show_header=False, box=None, padding=(0, 1)) |
| 58 | + |
| 59 | + details_table.add_column('Key', style='dim') |
| 60 | + details_table.add_column('Value', style='', overflow='fold') |
| 61 | + |
| 62 | + severity = detection.severity if detection.severity else 'N/A' |
| 63 | + severity_icon = SeverityOption.get_member_emoji(severity.lower()) |
| 64 | + details_table.add_row('Severity', f'{severity_icon} {SeverityOption(severity).__rich__()}') |
| 65 | + |
| 66 | + detection_details = detection.detection_details |
| 67 | + path = Path(detection_details.get('file_name', '')) |
| 68 | + details_table.add_row('In file', path.name) # it is name already except for IaC :) |
| 69 | + |
| 70 | + # we do not allow using rich output with SCA; SCA designed to be used with table output |
| 71 | + if self.scan_type == consts.IAC_SCAN_TYPE: |
| 72 | + details_table.add_row('IaC Provider', detection_details.get('infra_provider')) |
| 73 | + elif self.scan_type == consts.SECRET_SCAN_TYPE: |
| 74 | + details_table.add_row('Secret SHA', detection_details.get('sha512')) |
| 75 | + elif self.scan_type == consts.SAST_SCAN_TYPE: |
| 76 | + details_table.add_row('Subcategory', detection_details.get('category')) |
| 77 | + details_table.add_row('Language', ', '.join(detection_details.get('languages', []))) |
| 78 | + |
| 79 | + engine_id_to_display_name = { |
| 80 | + '5db84696-88dc-11ec-a8a3-0242ac120002': 'Semgrep OSS (Orchestrated by Cycode)', |
| 81 | + '560a0abd-d7da-4e6d-a3f1-0ed74895295c': 'Bearer (Powered by Cycode)', |
| 82 | + } |
| 83 | + engine_id = detection.detection_details.get('external_scanner_id') |
| 84 | + details_table.add_row('Security Tool', engine_id_to_display_name.get(engine_id, 'N/A')) |
| 85 | + |
| 86 | + details_table.add_row('Rule ID', detection.detection_rule_id) |
| 87 | + |
| 88 | + return details_table |
| 89 | + |
| 90 | + def _print_violation_card( |
| 91 | + self, document: 'Document', detection: 'Detection', detection_number: int, detections_count: int |
| 92 | + ) -> None: |
| 93 | + details_table = self._get_details_table(detection) |
| 94 | + details_panel = get_panel( |
| 95 | + details_table, |
| 96 | + title=':mag: Details', |
| 97 | + ) |
| 98 | + |
| 99 | + code_snippet_panel = get_panel( |
| 100 | + get_code_snippet_syntax( |
| 101 | + self.scan_type, |
| 102 | + self.command_scan_type, |
| 103 | + detection, |
| 104 | + document, |
| 105 | + obfuscate=not self.show_secret, |
| 106 | + ), |
| 107 | + title=':computer: Code Snippet', |
| 108 | + ) |
| 109 | + |
| 110 | + guidelines_panel = None |
| 111 | + guidelines = detection.detection_details.get('remediation_guidelines') |
| 112 | + if guidelines: |
| 113 | + guidelines_panel = get_markdown_panel( |
| 114 | + guidelines, |
| 115 | + title=':clipboard: Cycode Guidelines', |
| 116 | + ) |
| 117 | + |
| 118 | + custom_guidelines_panel = None |
| 119 | + custom_guidelines = detection.detection_details.get('custom_remediation_guidelines') |
| 120 | + if custom_guidelines: |
| 121 | + custom_guidelines_panel = get_markdown_panel( |
| 122 | + custom_guidelines, |
| 123 | + title=':office: Company Guidelines', |
| 124 | + ) |
| 125 | + |
| 126 | + navigation = Text(f'Violation {detection_number} of {detections_count}', style='dim', justify='right') |
| 127 | + |
| 128 | + renderables = [navigation, get_columns_in_1_to_3_ratio(details_panel, code_snippet_panel)] |
| 129 | + if guidelines_panel: |
| 130 | + renderables.append(guidelines_panel) |
| 131 | + if custom_guidelines_panel: |
| 132 | + renderables.append(custom_guidelines_panel) |
| 133 | + |
| 134 | + violation_card_panel = Panel( |
| 135 | + Group(*renderables), |
| 136 | + title=get_detection_title(self.scan_type, detection), |
| 137 | + border_style=SeverityOption.get_member_color(detection.severity), |
| 138 | + title_align='center', |
| 139 | + ) |
| 140 | + |
| 141 | + console.print(violation_card_panel) |
0 commit comments