11import math
2+ import urllib .parse
23from typing import TYPE_CHECKING , Dict , List , Optional
34
4- import click
55import typer
6+ from rich .console import Console
7+ from rich .markup import escape
8+ from rich .syntax import Syntax
69
10+ from cycode .cli .cli_types import SeverityOption
711from cycode .cli .consts import COMMIT_RANGE_BASED_COMMAND_SCAN_TYPES , SECRET_SCAN_TYPE
812from cycode .cli .models import CliError , CliResult , Detection , Document , DocumentDetections
913from cycode .cli .printers .printer_base import PrinterBase
@@ -25,73 +29,80 @@ def print_result(self, result: CliResult) -> None:
2529 if not result .success :
2630 color = self .RED_COLOR_NAME
2731
28- click .secho (result .message , fg = color )
32+ typer .secho (result .message , fg = color )
2933
3034 if not result .data :
3135 return
3236
33- click .secho ('\n Additional data:' , fg = color )
37+ typer .secho ('\n Additional data:' , fg = color )
3438 for name , value in result .data .items ():
35- click .secho (f'- { name } : { value } ' , fg = color )
39+ typer .secho (f'- { name } : { value } ' , fg = color )
3640
3741 def print_error (self , error : CliError ) -> None :
38- click .secho (error .message , fg = self .RED_COLOR_NAME )
42+ typer .secho (error .message , fg = self .RED_COLOR_NAME )
3943
4044 def print_scan_results (
4145 self , local_scan_results : List ['LocalScanResult' ], errors : Optional [Dict [str , 'CliError' ]] = None
4246 ) -> None :
4347 if not errors and all (result .issue_detected == 0 for result in local_scan_results ):
44- click .secho ('Good job! No issues were found!!! 👏👏👏' , fg = self .GREEN_COLOR_NAME )
48+ typer .secho ('Good job! No issues were found!!! 👏👏👏' , fg = self .GREEN_COLOR_NAME )
4549 return
4650
4751 for local_scan_result in local_scan_results :
4852 for document_detections in local_scan_result .document_detections :
49- self ._print_document_detections (document_detections , local_scan_result . scan_id )
53+ self ._print_document_detections (document_detections )
5054
5155 report_urls = [scan_result .report_url for scan_result in local_scan_results if scan_result .report_url ]
5256
5357 self ._print_report_urls (report_urls , self .ctx .obj .get ('aggregation_report_url' ))
5458 if not errors :
5559 return
5660
57- click .secho (
61+ typer .secho (
5862 'Unfortunately, Cycode was unable to complete the full scan. '
5963 'Please note that not all results may be available:' ,
6064 fg = 'red' ,
6165 )
6266 for scan_id , error in errors .items ():
63- click .echo (f'- { scan_id } : ' , nl = False )
67+ typer .echo (f'- { scan_id } : ' , nl = False )
6468 self .print_error (error )
6569
66- def _print_document_detections (self , document_detections : DocumentDetections , scan_id : str ) -> None :
70+ def _print_document_detections (self , document_detections : DocumentDetections ) -> None :
6771 document = document_detections .document
6872 for detection in document_detections .detections :
69- self ._print_detection_summary (detection , document .path , scan_id )
73+ self ._print_detection_summary (detection , document .path )
74+ self ._print_new_line ()
7075 self ._print_detection_code_segment (detection , document )
76+ self ._print_new_line ()
7177
72- def _print_detection_summary (self , detection : Detection , document_path : str , scan_id : str ) -> None :
78+ @staticmethod
79+ def _print_new_line () -> None :
80+ typer .echo ()
81+
82+ def _print_detection_summary (self , detection : Detection , document_path : str ) -> None :
7383 detection_name = detection .type if self .scan_type == SECRET_SCAN_TYPE else detection .message
74- detection_name_styled = click .style (detection_name , fg = 'bright_red' , bold = True )
7584
76- detection_sha = detection .detection_details .get ('sha512' )
77- detection_sha_message = f'\n Secret SHA: { detection_sha } ' if detection_sha else ''
85+ detection_severity = detection .severity or 'N/A'
86+ detection_severity_color = SeverityOption .get_member_color (detection_severity )
87+ detection_severity = f'[{ detection_severity_color } ]{ detection_severity .upper ()} [/{ detection_severity_color } ]'
88+
89+ escaped_document_path = escape (urllib .parse .quote (document_path ))
90+ clickable_document_path = f'[link file://{ escaped_document_path } ]{ document_path } '
7891
79- scan_id_message = f'\n Scan ID: { scan_id } '
8092 detection_commit_id = detection .detection_details .get ('commit_id' )
8193 detection_commit_id_message = f'\n Commit SHA: { detection_commit_id } ' if detection_commit_id else ''
8294
8395 company_guidelines = detection .detection_details .get ('custom_remediation_guidelines' )
8496 company_guidelines_message = f'\n Company Guideline: { company_guidelines } ' if company_guidelines else ''
8597
86- click .echo (
87- f'⛔ '
88- f'Found issue of type: { detection_name_styled } '
89- f'(rule ID: { detection .detection_rule_id } ) in file: { click .format_filename (document_path )} '
90- f'{ detection_sha_message } '
91- f'{ scan_id_message } '
98+ Console ().print (
99+ f':no_entry: '
100+ f'Found { detection_severity } issue of type: [bright_red][bold]{ detection_name } [/bold][/bright_red] '
101+ f'in file: { clickable_document_path } '
92102 f'{ detection_commit_id_message } '
93103 f'{ company_guidelines_message } '
94- f' ⛔'
104+ f' :no_entry:' ,
105+ highlight = True ,
95106 )
96107
97108 def _print_detection_code_segment (
@@ -109,145 +120,86 @@ def _print_report_urls(report_urls: List[str], aggregation_report_url: Optional[
109120 if not report_urls and not aggregation_report_url :
110121 return
111122 if aggregation_report_url :
112- click .echo (f'Report URL: { aggregation_report_url } ' )
123+ typer .echo (f'Report URL: { aggregation_report_url } ' )
113124 return
114125
115- click .echo ('Report URLs:' )
126+ typer .echo ('Report URLs:' )
116127 for report_url in report_urls :
117- click .echo (f'- { report_url } ' )
128+ typer .echo (f'- { report_url } ' )
118129
119130 @staticmethod
120131 def _get_code_segment_start_line (detection_line : int , lines_to_display : int ) -> int :
121132 start_line = detection_line - math .ceil (lines_to_display / 2 )
122133 return 0 if start_line < 0 else start_line
123134
124- def _print_line_of_code_segment (
125- self ,
126- document : Document ,
127- line : str ,
128- line_number : int ,
129- detection_position_in_line : int ,
130- violation_length : int ,
131- is_detection_line : bool ,
132- ) -> None :
133- if is_detection_line :
134- self ._print_detection_line (document , line , line_number , detection_position_in_line , violation_length )
135- else :
136- self ._print_line (document , line , line_number )
137-
138- def _print_detection_line (
139- self , document : Document , line : str , line_number : int , detection_position_in_line : int , violation_length : int
140- ) -> None :
141- detection_line = self ._get_detection_line_style (
142- line , document .is_git_diff_format , detection_position_in_line , violation_length
143- )
144-
145- click .echo (f'{ self ._get_line_number_style (line_number )} { detection_line } ' )
146-
147- def _print_line (self , document : Document , line : str , line_number : int ) -> None :
148- line_no = self ._get_line_number_style (line_number )
149- line = self ._get_line_style (line , document .is_git_diff_format )
150-
151- click .echo (f'{ line_no } { line } ' )
152-
153- def _get_detection_line_style (self , line : str , is_git_diff : bool , start_position : int , length : int ) -> str :
154- line_color = self ._get_line_color (line , is_git_diff )
155- if self .scan_type != SECRET_SCAN_TYPE or start_position < 0 or length < 0 :
156- return self ._get_line_style (line , is_git_diff , line_color )
157-
158- violation = line [start_position : start_position + length ]
159- if not self .show_secret :
160- violation = obfuscate_text (violation )
161-
162- line_to_violation = line [0 :start_position ]
163- line_from_violation = line [start_position + length :]
164-
165- return (
166- f'{ self ._get_line_style (line_to_violation , is_git_diff , line_color )} '
167- f'{ self ._get_line_style (violation , is_git_diff , line_color , underline = True )} '
168- f'{ self ._get_line_style (line_from_violation , is_git_diff , line_color )} '
169- )
170-
171- def _get_line_style (
172- self , line : str , is_git_diff : bool , color : Optional [str ] = None , underline : bool = False
173- ) -> str :
174- if color is None :
175- color = self ._get_line_color (line , is_git_diff )
176-
177- return click .style (line , fg = color , bold = False , underline = underline )
178-
179- def _get_line_color (self , line : str , is_git_diff : bool ) -> str :
180- if not is_git_diff :
181- return self .WHITE_COLOR_NAME
182-
183- if line .startswith ('+' ):
184- return self .GREEN_COLOR_NAME
185-
186- if line .startswith ('-' ):
187- return self .RED_COLOR_NAME
188-
189- return self .WHITE_COLOR_NAME
190-
191- def _get_line_number_style (self , line_number : int ) -> str :
135+ def _get_detection_line (self , detection : Detection ) -> int :
192136 return (
193- f'{ click .style (str (line_number ), fg = self .WHITE_COLOR_NAME , bold = False )} '
194- f'{ click .style ("|" , fg = self .RED_COLOR_NAME , bold = False )} '
137+ detection .detection_details .get ('line' , - 1 )
138+ if self .scan_type == SECRET_SCAN_TYPE
139+ else detection .detection_details .get ('line_in_file' , - 1 ) - 1
195140 )
196141
197142 def _print_detection_from_file (self , detection : Detection , document : Document , lines_to_display : int ) -> None :
198143 detection_details = detection .detection_details
199- detection_line = (
200- detection_details .get ('line' , - 1 )
201- if self .scan_type == SECRET_SCAN_TYPE
202- else detection_details .get ('line_in_file' , - 1 )
203- )
204- detection_position = detection_details .get ('start_position' , - 1 )
144+ detection_line = self ._get_detection_line (detection )
145+ start_line_index = self ._get_code_segment_start_line (detection_line , lines_to_display )
146+ detection_position = get_position_in_line (document .content , detection_details .get ('start_position' , - 1 ))
205147 violation_length = detection_details .get ('length' , - 1 )
206148
207- file_content = document .content
208- file_lines = file_content .splitlines ()
209- start_line = self ._get_code_segment_start_line (detection_line , lines_to_display )
210- detection_position_in_line = get_position_in_line (file_content , detection_position )
211-
212- click .echo ()
149+ code_lines_to_render = []
150+ document_content_lines = document .content .splitlines ()
213151 for line_index in range (lines_to_display ):
214- current_line_index = start_line + line_index
215- if current_line_index >= len (file_lines ):
152+ current_line_index = start_line_index + line_index
153+ if current_line_index >= len (document_content_lines ):
216154 break
217155
218- current_line = file_lines [current_line_index ]
219- is_detection_line = current_line_index == detection_line
220- self ._print_line_of_code_segment (
221- document ,
222- current_line ,
223- current_line_index + 1 ,
224- detection_position_in_line ,
225- violation_length ,
226- is_detection_line ,
156+ line_content = document_content_lines [current_line_index ]
157+
158+ line_with_detection = current_line_index == detection_line
159+ if self .scan_type == SECRET_SCAN_TYPE and line_with_detection and not self .show_secret :
160+ violation = line_content [detection_position : detection_position + violation_length ]
161+ code_lines_to_render .append (line_content .replace (violation , obfuscate_text (violation )))
162+ else :
163+ code_lines_to_render .append (line_content )
164+
165+ code_to_render = '\n ' .join (code_lines_to_render )
166+ Console ().print (
167+ Syntax (
168+ code = code_to_render ,
169+ lexer = Syntax .guess_lexer (document .path , code = code_to_render ),
170+ line_numbers = True ,
171+ dedent = True ,
172+ tab_size = 2 ,
173+ start_line = start_line_index + 1 ,
174+ highlight_lines = {
175+ detection_line + 1 ,
176+ },
227177 )
228- click . echo ( )
178+ )
229179
230180 def _print_detection_from_git_diff (self , detection : Detection , document : Document ) -> None :
231181 detection_details = detection .detection_details
232- detection_line_number = detection_details .get ('line' , - 1 )
233- detection_line_number_in_original_file = detection_details .get ('line_in_file' , - 1 )
182+ detection_line = self ._get_detection_line (detection )
234183 detection_position = detection_details .get ('start_position' , - 1 )
235184 violation_length = detection_details .get ('length' , - 1 )
236185
237- git_diff_content = document .content
238- git_diff_lines = git_diff_content .splitlines ()
239- detection_line = git_diff_lines [detection_line_number ]
240- detection_position_in_line = get_position_in_line (git_diff_content , detection_position )
241-
242- click .echo ()
243- self ._print_detection_line (
244- document ,
245- detection_line ,
246- detection_line_number_in_original_file ,
247- detection_position_in_line ,
248- violation_length ,
186+ line_content = document .content .splitlines ()[detection_line ]
187+ detection_position_in_line = get_position_in_line (document .content , detection_position )
188+ if self .scan_type == SECRET_SCAN_TYPE and not self .show_secret :
189+ violation = line_content [detection_position_in_line : detection_position_in_line + violation_length ]
190+ line_content = line_content .replace (violation , obfuscate_text (violation ))
191+
192+ Console ().print (
193+ Syntax (
194+ line_content ,
195+ lexer = 'diff' ,
196+ line_numbers = True ,
197+ start_line = detection_line ,
198+ dedent = True ,
199+ tab_size = 2 ,
200+ highlight_lines = {detection_line + 1 },
201+ )
249202 )
250- click .echo ()
251203
252204 def _is_git_diff_based_scan (self ) -> bool :
253205 return self .command_scan_type in COMMIT_RANGE_BASED_COMMAND_SCAN_TYPES and self .scan_type == SECRET_SCAN_TYPE
0 commit comments