Skip to content

Commit 72e8b77

Browse files
authored
CM-46137 - Add visual separators of row groups; reorder columns (#289)
1 parent da80ead commit 72e8b77

File tree

7 files changed

+103
-85
lines changed

7 files changed

+103
-85
lines changed

cycode/cli/apps/scan/code_scanner.py

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949

5050
def scan_sca_pre_commit(ctx: typer.Context) -> None:
5151
scan_type = ctx.obj['scan_type']
52-
scan_parameters = get_default_scan_parameters(ctx)
52+
scan_parameters = get_scan_parameters(ctx)
5353
git_head_documents, pre_committed_documents = get_pre_commit_modified_documents(
5454
ctx.obj['progress_bar'], ScanProgressBarSection.PREPARE_LOCAL_FILES
5555
)
@@ -83,15 +83,14 @@ def scan_sca_commit_range(ctx: typer.Context, path: str, commit_range: str) -> N
8383
scan_commit_range_documents(ctx, from_commit_documents, to_commit_documents, scan_parameters=scan_parameters)
8484

8585

86-
def scan_disk_files(ctx: typer.Context, paths: Tuple[str, ...]) -> None:
87-
scan_parameters = get_scan_parameters(ctx, paths)
86+
def scan_disk_files(ctx: click.Context, paths: Tuple[str]) -> None:
8887
scan_type = ctx.obj['scan_type']
8988
progress_bar = ctx.obj['progress_bar']
9089

9190
try:
9291
documents = get_relevant_documents(progress_bar, ScanProgressBarSection.PREPARE_LOCAL_FILES, scan_type, paths)
9392
perform_pre_scan_documents_actions(ctx, scan_type, documents)
94-
scan_documents(ctx, documents, scan_parameters=scan_parameters)
93+
scan_documents(ctx, documents, get_scan_parameters(ctx, paths))
9594
except Exception as e:
9695
handle_scan_exception(ctx, e)
9796

@@ -155,14 +154,12 @@ def _enrich_scan_result_with_data_from_detection_rules(
155154

156155
def _get_scan_documents_thread_func(
157156
ctx: typer.Context, is_git_diff: bool, is_commit_range: bool, scan_parameters: dict
158-
) -> Tuple[Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]], str]:
157+
) -> Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]]:
159158
cycode_client = ctx.obj['client']
160159
scan_type = ctx.obj['scan_type']
161160
severity_threshold = ctx.obj['severity_threshold']
162161
sync_option = ctx.obj['sync']
163162
command_scan_type = ctx.info_name
164-
aggregation_id = str(_generate_unique_id())
165-
scan_parameters['aggregation_id'] = aggregation_id
166163

167164
def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, LocalScanResult]:
168165
local_scan_result = error = error_message = None
@@ -231,7 +228,7 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local
231228

232229
return scan_id, error, local_scan_result
233230

234-
return _scan_batch_thread_func, aggregation_id
231+
return _scan_batch_thread_func
235232

236233

237234
def scan_commit_range(
@@ -291,20 +288,17 @@ def scan_commit_range(
291288
logger.debug('List of commit ids to scan, %s', {'commit_ids': commit_ids_to_scan})
292289
logger.debug('Starting to scan commit range (it may take a few minutes)')
293290

294-
scan_documents(ctx, documents_to_scan, is_git_diff=True, is_commit_range=True)
291+
scan_documents(ctx, documents_to_scan, get_scan_parameters(ctx, (path,)), is_git_diff=True, is_commit_range=True)
295292
return None
296293

297294

298295
def scan_documents(
299296
ctx: typer.Context,
300297
documents_to_scan: List[Document],
298+
scan_parameters: dict,
301299
is_git_diff: bool = False,
302300
is_commit_range: bool = False,
303-
scan_parameters: Optional[dict] = None,
304301
) -> None:
305-
if not scan_parameters:
306-
scan_parameters = get_default_scan_parameters(ctx)
307-
308302
scan_type = ctx.obj['scan_type']
309303
progress_bar = ctx.obj['progress_bar']
310304

@@ -319,19 +313,13 @@ def scan_documents(
319313
)
320314
return
321315

322-
scan_batch_thread_func, aggregation_id = _get_scan_documents_thread_func(
323-
ctx, is_git_diff, is_commit_range, scan_parameters
324-
)
316+
scan_batch_thread_func = _get_scan_documents_thread_func(ctx, is_git_diff, is_commit_range, scan_parameters)
325317
errors, local_scan_results = run_parallel_batched_scan(
326318
scan_batch_thread_func, scan_type, documents_to_scan, progress_bar=progress_bar
327319
)
328320

329-
if len(local_scan_results) > 1:
330-
# if we used more than one batch, we need to fetch aggregate report url
331-
aggregation_report_url = _try_get_aggregation_report_url_if_needed(
332-
scan_parameters, ctx.obj['client'], scan_type
333-
)
334-
set_aggregation_report_url(ctx, aggregation_report_url)
321+
aggregation_report_url = _try_get_aggregation_report_url_if_needed(scan_parameters, ctx.obj['client'], scan_type)
322+
_set_aggregation_report_url(ctx, aggregation_report_url)
335323

336324
progress_bar.set_section_length(ScanProgressBarSection.GENERATE_REPORT, 1)
337325
progress_bar.update(ScanProgressBarSection.GENERATE_REPORT)
@@ -341,25 +329,6 @@ def scan_documents(
341329
print_results(ctx, local_scan_results, errors)
342330

343331

344-
def set_aggregation_report_url(ctx: typer.Context, aggregation_report_url: Optional[str] = None) -> None:
345-
ctx.obj['aggregation_report_url'] = aggregation_report_url
346-
347-
348-
def _try_get_aggregation_report_url_if_needed(
349-
scan_parameters: dict, cycode_client: 'ScanClient', scan_type: str
350-
) -> Optional[str]:
351-
aggregation_id = scan_parameters.get('aggregation_id')
352-
if not scan_parameters.get('report'):
353-
return None
354-
if aggregation_id is None:
355-
return None
356-
try:
357-
report_url_response = cycode_client.get_scan_aggregation_report_url(aggregation_id, scan_type)
358-
return report_url_response.report_url
359-
except Exception as e:
360-
logger.debug('Failed to get aggregation report url: %s', str(e))
361-
362-
363332
def scan_commit_range_documents(
364333
ctx: typer.Context,
365334
from_documents_to_scan: List[Document],
@@ -384,7 +353,7 @@ def scan_commit_range_documents(
384353
try:
385354
progress_bar.set_section_length(ScanProgressBarSection.SCAN, 1)
386355

387-
scan_result = init_default_scan_result(cycode_client, scan_id, scan_type)
356+
scan_result = init_default_scan_result(scan_id)
388357
if should_scan_documents(from_documents_to_scan, to_documents_to_scan):
389358
logger.debug('Preparing from-commit zip')
390359
from_commit_zipped_documents = zip_documents(scan_type, from_documents_to_scan)
@@ -522,7 +491,7 @@ def perform_scan_async(
522491
cycode_client,
523492
scan_async_result.scan_id,
524493
scan_type,
525-
scan_parameters.get('report'),
494+
scan_parameters,
526495
)
527496

528497

@@ -557,16 +526,14 @@ def perform_commit_range_scan_async(
557526
logger.debug(
558527
'Async commit range scan request has been triggered successfully, %s', {'scan_id': scan_async_result.scan_id}
559528
)
560-
return poll_scan_results(
561-
cycode_client, scan_async_result.scan_id, scan_type, scan_parameters.get('report'), timeout
562-
)
529+
return poll_scan_results(cycode_client, scan_async_result.scan_id, scan_type, scan_parameters, timeout)
563530

564531

565532
def poll_scan_results(
566533
cycode_client: 'ScanClient',
567534
scan_id: str,
568535
scan_type: str,
569-
should_get_report: bool = False,
536+
scan_parameters: dict,
570537
polling_timeout: Optional[int] = None,
571538
) -> ZippedFileScanResult:
572539
if polling_timeout is None:
@@ -583,7 +550,7 @@ def poll_scan_results(
583550
print_debug_scan_details(scan_details)
584551

585552
if scan_details.scan_status == consts.SCAN_STATUS_COMPLETED:
586-
return _get_scan_result(cycode_client, scan_type, scan_id, scan_details, should_get_report)
553+
return _get_scan_result(cycode_client, scan_type, scan_id, scan_details, scan_parameters)
587554

588555
if scan_details.scan_status == consts.SCAN_STATUS_ERROR:
589556
raise custom_exceptions.ScanAsyncError(
@@ -675,18 +642,19 @@ def parse_pre_receive_input() -> str:
675642
return pre_receive_input.splitlines()[0]
676643

677644

678-
def get_default_scan_parameters(ctx: typer.Context) -> dict:
645+
def _get_default_scan_parameters(ctx: click.Context) -> dict:
679646
return {
680647
'monitor': ctx.obj.get('monitor'),
681648
'report': ctx.obj.get('report'),
682649
'package_vulnerabilities': ctx.obj.get('package-vulnerabilities'),
683650
'license_compliance': ctx.obj.get('license-compliance'),
684651
'command_type': ctx.info_name,
652+
'aggregation_id': str(_generate_unique_id()),
685653
}
686654

687655

688-
def get_scan_parameters(ctx: typer.Context, paths: Tuple[str, ...]) -> dict:
689-
scan_parameters = get_default_scan_parameters(ctx)
656+
def get_scan_parameters(ctx: typer.Context, paths: Optional[Tuple[str]] = None) -> dict:
657+
scan_parameters = _get_default_scan_parameters(ctx)
690658

691659
if not paths:
692660
return scan_parameters
@@ -894,36 +862,51 @@ def _get_scan_result(
894862
scan_type: str,
895863
scan_id: str,
896864
scan_details: 'ScanDetailsResponse',
897-
should_get_report: bool = False,
865+
scan_parameters: dict,
898866
) -> ZippedFileScanResult:
899867
if not scan_details.detections_count:
900-
return init_default_scan_result(cycode_client, scan_id, scan_type, should_get_report)
868+
return init_default_scan_result(scan_id)
901869

902870
scan_raw_detections = cycode_client.get_scan_raw_detections(scan_type, scan_id)
903871

904872
return ZippedFileScanResult(
905873
did_detect=True,
906874
detections_per_file=_map_detections_per_file_and_commit_id(scan_type, scan_raw_detections),
907875
scan_id=scan_id,
908-
report_url=_try_get_report_url_if_needed(cycode_client, should_get_report, scan_id, scan_type),
876+
report_url=_try_get_any_report_url_if_needed(cycode_client, scan_id, scan_type, scan_parameters),
909877
)
910878

911879

912-
def init_default_scan_result(
913-
cycode_client: 'ScanClient', scan_id: str, scan_type: str, should_get_report: bool = False
914-
) -> ZippedFileScanResult:
880+
def init_default_scan_result(scan_id: str) -> ZippedFileScanResult:
915881
return ZippedFileScanResult(
916882
did_detect=False,
917883
detections_per_file=[],
918884
scan_id=scan_id,
919-
report_url=_try_get_report_url_if_needed(cycode_client, should_get_report, scan_id, scan_type),
920885
)
921886

922887

888+
def _try_get_any_report_url_if_needed(
889+
cycode_client: 'ScanClient',
890+
scan_id: str,
891+
scan_type: str,
892+
scan_parameters: dict,
893+
) -> Optional[str]:
894+
"""Tries to get aggregation report URL if needed, otherwise tries to get report URL."""
895+
aggregation_report_url = None
896+
if scan_parameters:
897+
_try_get_report_url_if_needed(cycode_client, scan_id, scan_type, scan_parameters)
898+
aggregation_report_url = _try_get_aggregation_report_url_if_needed(scan_parameters, cycode_client, scan_type)
899+
900+
if aggregation_report_url:
901+
return aggregation_report_url
902+
903+
return _try_get_report_url_if_needed(cycode_client, scan_id, scan_type, scan_parameters)
904+
905+
923906
def _try_get_report_url_if_needed(
924-
cycode_client: 'ScanClient', should_get_report: bool, scan_id: str, scan_type: str
907+
cycode_client: 'ScanClient', scan_id: str, scan_type: str, scan_parameters: dict
925908
) -> Optional[str]:
926-
if not should_get_report:
909+
if not scan_parameters.get('report', False):
927910
return None
928911

929912
try:
@@ -933,6 +916,27 @@ def _try_get_report_url_if_needed(
933916
logger.debug('Failed to get report URL', exc_info=e)
934917

935918

919+
def _set_aggregation_report_url(ctx: click.Context, aggregation_report_url: Optional[str] = None) -> None:
920+
ctx.obj['aggregation_report_url'] = aggregation_report_url
921+
922+
923+
def _try_get_aggregation_report_url_if_needed(
924+
scan_parameters: dict, cycode_client: 'ScanClient', scan_type: str
925+
) -> Optional[str]:
926+
if not scan_parameters.get('report', False):
927+
return None
928+
929+
aggregation_id = scan_parameters.get('aggregation_id')
930+
if aggregation_id is None:
931+
return None
932+
933+
try:
934+
report_url_response = cycode_client.get_scan_aggregation_report_url(aggregation_id, scan_type)
935+
return report_url_response.report_url
936+
except Exception as e:
937+
logger.debug('Failed to get aggregation report url: %s', str(e))
938+
939+
936940
def _map_detections_per_file_and_commit_id(scan_type: str, raw_detections: List[dict]) -> List[DetectionsPerFile]:
937941
"""Converts list of detections (async flow) to list of DetectionsPerFile objects (sync flow).
938942

cycode/cli/apps/scan/pre_commit/pre_commit_command.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import typer
55

66
from cycode.cli import consts
7-
from cycode.cli.apps.scan.code_scanner import scan_documents, scan_sca_pre_commit
7+
from cycode.cli.apps.scan.code_scanner import get_scan_parameters, scan_documents, scan_sca_pre_commit
88
from cycode.cli.files_collector.excluder import exclude_irrelevant_documents_to_scan
99
from cycode.cli.files_collector.repository_documents import (
1010
get_diff_file_content,
@@ -44,4 +44,4 @@ def pre_commit_command(
4444
documents_to_scan.append(Document(get_path_by_os(get_diff_file_path(file)), get_diff_file_content(file)))
4545

4646
documents_to_scan = exclude_irrelevant_documents_to_scan(scan_type, documents_to_scan)
47-
scan_documents(ctx, documents_to_scan, is_git_diff=True)
47+
scan_documents(ctx, documents_to_scan, get_scan_parameters(ctx), is_git_diff=True)

cycode/cli/apps/scan/repository/repository_command.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ def repository_command(
6363
perform_pre_scan_documents_actions(ctx, scan_type, documents_to_scan)
6464

6565
logger.debug('Found all relevant files for scanning %s', {'path': path, 'branch': branch})
66-
scan_parameters = get_scan_parameters(ctx, (str(path),))
67-
scan_documents(ctx, documents_to_scan, scan_parameters=scan_parameters)
66+
scan_documents(ctx, documents_to_scan, get_scan_parameters(ctx, (path,)))
6867
except Exception as e:
6968
handle_scan_exception(ctx, e)

cycode/cli/printers/tables/sca_table_printer.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections import defaultdict
2-
from typing import TYPE_CHECKING, Dict, List
2+
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
33

44
import typer
55

@@ -37,9 +37,12 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
3737
for policy_id, detections in detections_per_policy_id.items():
3838
table = self._get_table(policy_id)
3939

40-
for detection in self._sort_and_group_detections(detections):
40+
resulting_detections, group_separator_indexes = self._sort_and_group_detections(detections)
41+
for detection in resulting_detections:
4142
self._enrich_table_with_values(policy_id, table, detection)
4243

44+
table.set_group_separator_indexes(group_separator_indexes)
45+
4346
self._print_summary_issues(len(detections), self._get_title(policy_id))
4447
self._print_table(table)
4548

@@ -76,7 +79,7 @@ def __package_sort_key(detection: Detection) -> int:
7679
def _sort_detections_by_package(self, detections: List[Detection]) -> List[Detection]:
7780
return sorted(detections, key=self.__package_sort_key)
7881

79-
def _sort_and_group_detections(self, detections: List[Detection]) -> List[Detection]:
82+
def _sort_and_group_detections(self, detections: List[Detection]) -> Tuple[List[Detection], Set[int]]:
8083
"""Sort detections by severity and group by repository, code project and package name.
8184
8285
Note:
@@ -85,7 +88,8 @@ def _sort_and_group_detections(self, detections: List[Detection]) -> List[Detect
8588
Grouping by code projects also groups by ecosystem.
8689
Because manifest files are unique per ecosystem.
8790
"""
88-
result = []
91+
resulting_detections = []
92+
group_separator_indexes = set()
8993

9094
# we sort detections by package name to make persist output order
9195
sorted_detections = self._sort_detections_by_package(detections)
@@ -96,9 +100,10 @@ def _sort_and_group_detections(self, detections: List[Detection]) -> List[Detect
96100
for code_project_group in grouped_by_code_project.values():
97101
grouped_by_package = self.__group_by(code_project_group, 'package_name')
98102
for package_group in grouped_by_package.values():
99-
result.extend(self._sort_detections_by_severity(package_group))
103+
group_separator_indexes.add(len(resulting_detections) - 1) # indexing starts from 0
104+
resulting_detections.extend(self._sort_detections_by_severity(package_group))
100105

101-
return result
106+
return resulting_detections, group_separator_indexes
102107

103108
def _get_table(self, policy_id: str) -> Table:
104109
table = Table()

cycode/cli/printers/tables/table.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import urllib.parse
2-
from typing import TYPE_CHECKING, Dict, List, Optional
2+
from typing import TYPE_CHECKING, Dict, List, Optional, Set
33

44
from rich.markup import escape
55
from rich.table import Table as RichTable
@@ -12,6 +12,8 @@ class Table:
1212
"""Helper class to manage columns and their values in the right order and only if the column should be presented."""
1313

1414
def __init__(self, column_infos: Optional[List['ColumnInfo']] = None) -> None:
15+
self._group_separator_indexes: Set[int] = set()
16+
1517
self._columns: Dict['ColumnInfo', List[str]] = {}
1618
if column_infos:
1719
self._columns = {columns: [] for columns in column_infos}
@@ -35,6 +37,9 @@ def add_file_path_cell(self, column: 'ColumnInfo', path: str) -> None:
3537
escaped_path = escape(encoded_path)
3638
self._add_cell_no_error(column, f'[link file://{escaped_path}]{path}')
3739

40+
def set_group_separator_indexes(self, group_separator_indexes: Set[int]) -> None:
41+
self._group_separator_indexes = group_separator_indexes
42+
3843
def _get_ordered_columns(self) -> List['ColumnInfo']:
3944
# we are sorting columns by index to make sure that columns will be printed in the right order
4045
return sorted(self._columns, key=lambda column_info: column_info.index)
@@ -53,7 +58,7 @@ def get_table(self) -> 'RichTable':
5358
extra_args = column.column_opts if column.column_opts else {}
5459
table.add_column(header=column.name, overflow='fold', **extra_args)
5560

56-
for raw in self.get_rows():
57-
table.add_row(*raw)
61+
for index, raw in enumerate(self.get_rows()):
62+
table.add_row(*raw, end_section=index in self._group_separator_indexes)
5863

5964
return table

0 commit comments

Comments
 (0)