44import click
55
66from cycode .cli .consts import LICENSE_COMPLIANCE_POLICY_ID , PACKAGE_VULNERABILITY_POLICY_ID
7- from cycode .cli .models import Detection
7+ from cycode .cli .models import Detection , Severity
88from cycode .cli .printers .tables .table import Table
99from cycode .cli .printers .tables .table_models import ColumnInfoBuilder , ColumnWidths
1010from cycode .cli .printers .tables .table_printer_base import TablePrinterBase
1919# Building must have strict order. Represents the order of the columns in the table (from left to right)
2020SEVERITY_COLUMN = column_builder .build (name = 'Severity' )
2121REPOSITORY_COLUMN = column_builder .build (name = 'Repository' )
22-
23- FILE_PATH_COLUMN = column_builder .build (name = 'File Path' )
22+ CODE_PROJECT_COLUMN = column_builder .build (name = 'Code Project' ) # File path to manifest file
2423ECOSYSTEM_COLUMN = column_builder .build (name = 'Ecosystem' )
25- DEPENDENCY_NAME_COLUMN = column_builder .build (name = 'Dependency Name' )
26- DIRECT_DEPENDENCY_COLUMN = column_builder .build (name = 'Direct Dependency' )
27- DEVELOPMENT_DEPENDENCY_COLUMN = column_builder .build (name = 'Development Dependency' )
28- DEPENDENCY_PATHS_COLUMN = column_builder .build (name = 'Dependency Paths' )
29-
24+ PACKAGE_COLUMN = column_builder .build (name = 'Package' )
3025CVE_COLUMNS = column_builder .build (name = 'CVE' )
26+ DEPENDENCY_PATHS_COLUMN = column_builder .build (name = 'Dependency Paths' )
3127UPGRADE_COLUMN = column_builder .build (name = 'Upgrade' )
3228LICENSE_COLUMN = column_builder .build (name = 'License' )
29+ DIRECT_DEPENDENCY_COLUMN = column_builder .build (name = 'Direct Dependency' )
30+ DEVELOPMENT_DEPENDENCY_COLUMN = column_builder .build (name = 'Development Dependency' )
31+
3332
3433COLUMN_WIDTHS_CONFIG : ColumnWidths = {
3534 REPOSITORY_COLUMN : 2 ,
36- FILE_PATH_COLUMN : 3 ,
35+ CODE_PROJECT_COLUMN : 2 ,
36+ PACKAGE_COLUMN : 3 ,
3737 CVE_COLUMNS : 5 ,
3838 UPGRADE_COLUMN : 3 ,
3939 LICENSE_COLUMN : 2 ,
@@ -47,7 +47,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
4747 table = self ._get_table (policy_id )
4848 table .set_cols_width (COLUMN_WIDTHS_CONFIG )
4949
50- for detection in detections :
50+ for detection in self . _sort_and_group_detections ( detections ) :
5151 self ._enrich_table_with_values (table , detection )
5252
5353 self ._print_summary_issues (len (detections ), self ._get_title (policy_id ))
@@ -64,6 +64,52 @@ def _get_title(policy_id: str) -> str:
6464
6565 return 'Unknown'
6666
67+ @staticmethod
68+ def __group_by (detections : List [Detection ], details_field_name : str ) -> Dict [str , List [Detection ]]:
69+ grouped = defaultdict (list )
70+ for detection in detections :
71+ grouped [detection .detection_details .get (details_field_name )].append (detection )
72+ return grouped
73+
74+ @staticmethod
75+ def __severity_sort_key (detection : Detection ) -> int :
76+ severity = detection .detection_details .get ('advisory_severity' )
77+ return Severity .try_get_value (severity )
78+
79+ def _sort_detections_by_severity (self , detections : List [Detection ]) -> List [Detection ]:
80+ return sorted (detections , key = self .__severity_sort_key , reverse = True )
81+
82+ @staticmethod
83+ def __package_sort_key (detection : Detection ) -> int :
84+ return detection .detection_details .get ('package_name' )
85+
86+ def _sort_detections_by_package (self , detections : List [Detection ]) -> List [Detection ]:
87+ return sorted (detections , key = self .__package_sort_key )
88+
89+ def _sort_and_group_detections (self , detections : List [Detection ]) -> List [Detection ]:
90+ """Sort detections by severity and group by repository, code project and package name.
91+
92+ Note:
93+ Code Project is path to manifest file.
94+
95+ Grouping by code projects also groups by ecosystem.
96+ Because manifest files are unique per ecosystem.
97+ """
98+ result = []
99+
100+ # we sort detections by package name to make persist output order
101+ sorted_detections = self ._sort_detections_by_package (detections )
102+
103+ grouped_by_repository = self .__group_by (sorted_detections , 'repository_name' )
104+ for repository_group in grouped_by_repository .values ():
105+ grouped_by_code_project = self .__group_by (repository_group , 'file_name' )
106+ for code_project_group in grouped_by_code_project .values ():
107+ grouped_by_package = self .__group_by (code_project_group , 'package_name' )
108+ for package_group in grouped_by_package .values ():
109+ result .extend (self ._sort_detections_by_severity (package_group ))
110+
111+ return result
112+
67113 def _get_table (self , policy_id : str ) -> Table :
68114 table = Table ()
69115
@@ -77,9 +123,9 @@ def _get_table(self, policy_id: str) -> Table:
77123 if self ._is_git_repository ():
78124 table .add (REPOSITORY_COLUMN )
79125
80- table .add (FILE_PATH_COLUMN )
126+ table .add (CODE_PROJECT_COLUMN )
81127 table .add (ECOSYSTEM_COLUMN )
82- table .add (DEPENDENCY_NAME_COLUMN )
128+ table .add (PACKAGE_COLUMN )
83129 table .add (DIRECT_DEPENDENCY_COLUMN )
84130 table .add (DEVELOPMENT_DEPENDENCY_COLUMN )
85131 table .add (DEPENDENCY_PATHS_COLUMN )
@@ -93,9 +139,9 @@ def _enrich_table_with_values(table: Table, detection: Detection) -> None:
93139 table .set (SEVERITY_COLUMN , detection_details .get ('advisory_severity' ))
94140 table .set (REPOSITORY_COLUMN , detection_details .get ('repository_name' ))
95141
96- table .set (FILE_PATH_COLUMN , detection_details .get ('file_name' ))
142+ table .set (CODE_PROJECT_COLUMN , detection_details .get ('file_name' ))
97143 table .set (ECOSYSTEM_COLUMN , detection_details .get ('ecosystem' ))
98- table .set (DEPENDENCY_NAME_COLUMN , detection_details .get ('package_name' ))
144+ table .set (PACKAGE_COLUMN , detection_details .get ('package_name' ))
99145 table .set (DIRECT_DEPENDENCY_COLUMN , detection_details .get ('is_direct_dependency_str' ))
100146 table .set (DEVELOPMENT_DEPENDENCY_COLUMN , detection_details .get ('is_dev_dependency_str' ))
101147
0 commit comments