Skip to content

Commit 0405c6b

Browse files
authored
CM-47493 - Make changes in CLI v3.0.0 after feedback (#299)
1 parent 6c70c21 commit 0405c6b

21 files changed

+214
-90
lines changed

cycode/cli/app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Annotated, Optional
44

55
import typer
6+
from typer import rich_utils
67
from typer.completion import install_callback, show_callback
78

89
from cycode import __version__
@@ -18,11 +19,18 @@
1819
from cycode.cyclient.models import UserAgentOptionScheme
1920
from cycode.logger import set_logging_level
2021

22+
# By default, it uses dim style which is hard to read with the combination of color from RICH_HELP
23+
rich_utils.STYLE_ERRORS_SUGGESTION = 'bold'
24+
# By default, it uses blue color which is too dark for some terminals
25+
rich_utils.RICH_HELP = "Try [cyan]'{command_path} {help_option}'[/] for help."
26+
27+
2128
app = typer.Typer(
2229
pretty_exceptions_show_locals=False,
2330
pretty_exceptions_short=True,
2431
context_settings=CLI_CONTEXT_SETTINGS,
2532
rich_markup_mode='rich',
33+
no_args_is_help=True,
2634
add_completion=False, # we add it manually to control the rich help panel
2735
)
2836

cycode/cli/apps/ai_remediation/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from cycode.cli.apps.ai_remediation.ai_remediation_command import ai_remediation_command
44

5-
app = typer.Typer()
5+
app = typer.Typer(no_args_is_help=True)
66
app.command(name='ai-remediation', short_help='Get AI remediation (INTERNAL).', hidden=True)(ai_remediation_command)
77

88
# backward compatibility

cycode/cli/apps/auth/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
app = typer.Typer(
77
name='auth',
88
help='Authenticate your machine to associate the CLI with your Cycode account.',
9+
no_args_is_help=True,
910
)
1011
app.callback(invoke_without_command=True)(auth_command)
1112
app.command(name='check')(check_command)

cycode/cli/apps/configure/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from cycode.cli.apps.configure.configure_command import configure_command
44

5-
app = typer.Typer()
5+
app = typer.Typer(no_args_is_help=True)
66
app.command(name='configure', short_help='Initial command to configure your CLI client authentication.')(
77
configure_command
88
)

cycode/cli/apps/ignore/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
from cycode.cli.apps.ignore.ignore_command import ignore_command
44

5-
app = typer.Typer()
5+
app = typer.Typer(no_args_is_help=True)
66
app.command(name='ignore', short_help='Ignores a specific value, path or rule ID.')(ignore_command)

cycode/cli/apps/report/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
from cycode.cli.apps.report import sbom
44
from cycode.cli.apps.report.report_command import report_command
55

6-
app = typer.Typer(name='report')
6+
app = typer.Typer(name='report', no_args_is_help=True)
77
app.callback(short_help='Generate report. You`ll need to specify which report type to perform.')(report_command)
88
app.add_typer(sbom.app)

cycode/cli/apps/scan/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from cycode.cli.apps.scan.repository.repository_command import repository_command
88
from cycode.cli.apps.scan.scan_command import scan_command, scan_command_result_callback
99

10-
app = typer.Typer(name='scan')
10+
app = typer.Typer(name='scan', no_args_is_help=True)
1111

1212
app.callback(
1313
short_help='Scan the content for Secrets, IaC, SCA, and SAST violations.',

cycode/cli/apps/status/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
from cycode.cli.apps.status.status_command import status_command
44
from cycode.cli.apps.status.version_command import version_command
55

6-
app = typer.Typer()
6+
app = typer.Typer(no_args_is_help=True)
77
app.command(name='status', short_help='Show the CLI status and exit.')(status_command)
88
app.command(name='version', hidden=True, short_help='Alias to status command.')(version_command)

cycode/cli/cli_types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,6 @@ def __rich__(self) -> str:
9494
SeverityOption.INFO.value: ':blue_circle:',
9595
SeverityOption.LOW.value: ':yellow_circle:',
9696
SeverityOption.MEDIUM.value: ':orange_circle:',
97-
SeverityOption.HIGH.value: ':heavy_large_circle:',
98-
SeverityOption.CRITICAL.value: ':red_circle:',
97+
SeverityOption.HIGH.value: ':red_circle:',
98+
SeverityOption.CRITICAL.value: ':exclamation_mark:', # double_exclamation_mark is not red
9999
}

cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ def get_lock_file_name(self) -> str:
3737
def verify_restore_file_already_exist(self, restore_file_path: str) -> bool:
3838
return os.path.isfile(restore_file_path)
3939

40-
def prepare_manifest_file_path_for_command(self, manifest_file_path: str) -> str:
40+
@staticmethod
41+
def prepare_manifest_file_path_for_command(manifest_file_path: str) -> str:
4142
return manifest_file_path.replace(os.sep + NPM_MANIFEST_FILE_NAME, '')

cycode/cli/files_collector/sca/sca_code_scanner.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,15 @@ def try_restore_dependencies(
124124
def add_dependencies_tree_document(
125125
ctx: typer.Context, documents_to_scan: List[Document], is_git_diff: bool = False
126126
) -> None:
127-
documents_to_add: Dict[str, Document] = {}
127+
documents_to_add: Dict[str, Document] = {document.path: document for document in documents_to_scan}
128128
restore_dependencies_list = restore_handlers(ctx, is_git_diff)
129129

130130
for restore_dependencies in restore_dependencies_list:
131131
for document in documents_to_scan:
132132
try_restore_dependencies(ctx, documents_to_add, restore_dependencies, document)
133133

134-
documents_to_scan.extend(list(documents_to_add.values()))
134+
# mutate original list using slice assignment
135+
documents_to_scan[:] = list(documents_to_add.values())
135136

136137

137138
def restore_handlers(ctx: typer.Context, is_git_diff: bool) -> List[BaseRestoreDependencies]:

cycode/cli/printers/console_printer.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class ConsolePrinter:
2929
# overrides
3030
'table_sca': ScaTablePrinter,
3131
'text_sca': ScaTablePrinter,
32-
'rich_sca': ScaTablePrinter,
3332
}
3433

3534
def __init__(
@@ -42,12 +41,7 @@ def __init__(
4241
self.ctx = ctx
4342
self.console = console_override or console
4443
self.console_err = console_err_override or console_err
45-
46-
self.scan_type = self.ctx.obj.get('scan_type')
4744
self.output_type = output_type_override or self.ctx.obj.get('output')
48-
self.aggregation_report_url = self.ctx.obj.get('aggregation_report_url')
49-
50-
self.printer = self._get_scan_printer()
5145

5246
self.console_record = None
5347

@@ -61,7 +55,16 @@ def __init__(
6155
output_type_override='json' if self.export_type == 'json' else self.output_type,
6256
)
6357

64-
def _get_scan_printer(self) -> 'PrinterBase':
58+
@property
59+
def scan_type(self) -> str:
60+
return self.ctx.obj.get('scan_type')
61+
62+
@property
63+
def aggregation_report_url(self) -> str:
64+
return self.ctx.obj.get('aggregation_report_url')
65+
66+
@property
67+
def printer(self) -> 'PrinterBase':
6568
printer_class = self._AVAILABLE_PRINTERS.get(self.output_type)
6669

6770
composite_printer = self._AVAILABLE_PRINTERS.get(f'{self.output_type}_{self.scan_type}')

cycode/cli/printers/printer_base.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import sys
22
from abc import ABC, abstractmethod
3+
from collections import defaultdict
34
from typing import TYPE_CHECKING, Dict, List, Optional
45

56
import typer
67

8+
from cycode.cli.cli_types import SeverityOption
79
from cycode.cli.models import CliError, CliResult
810
from cycode.cyclient.headers import get_correlation_id
911

@@ -35,6 +37,18 @@ def __init__(
3537
self.console = console
3638
self.console_err = console_err
3739

40+
@property
41+
def scan_type(self) -> str:
42+
return self.ctx.obj.get('scan_type')
43+
44+
@property
45+
def command_scan_type(self) -> str:
46+
return self.ctx.info_name
47+
48+
@property
49+
def show_secret(self) -> bool:
50+
return self.ctx.obj.get('show_secret', False)
51+
3852
@abstractmethod
3953
def print_scan_results(
4054
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:
6478
self.console_err.print(rich_traceback)
6579

6680
self.console_err.print(f'[red]Correlation ID:[/] {get_correlation_id()}')
81+
82+
def print_scan_results_summary(self, local_scan_results: List['LocalScanResult']) -> None:
83+
"""Print a summary of scan results based on severity levels.
84+
85+
Args:
86+
local_scan_results (List['LocalScanResult']): A list of local scan results containing detections.
87+
88+
The summary includes the count of detections for each severity level
89+
and is displayed in the console in a formatted string.
90+
"""
91+
92+
detections_count = 0
93+
severity_counts = defaultdict(int)
94+
for local_scan_result in local_scan_results:
95+
for document_detections in local_scan_result.document_detections:
96+
for detection in document_detections.detections:
97+
if detection.severity:
98+
detections_count += 1
99+
severity_counts[SeverityOption(detection.severity)] += 1
100+
101+
self.console.print(f'[bold]Cycode found {detections_count} violations[/]', end=': ')
102+
103+
# Example of string: CRITICAL - 6 | HIGH - 0 | MEDIUM - 14 | LOW - 0 | INFO - 0
104+
for index, severity in enumerate(reversed(SeverityOption), start=1):
105+
end = ' | '
106+
if index == len(SeverityOption):
107+
end = '\n'
108+
109+
self.console.print(
110+
SeverityOption.get_member_emoji(severity), severity, '-', severity_counts[severity], end=end
111+
)

cycode/cli/printers/rich_printer.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from pathlib import Path
21
from typing import TYPE_CHECKING, Dict, List, Optional
32

43
from rich.console import Group
@@ -10,7 +9,11 @@
109
from cycode.cli.cli_types import SeverityOption
1110
from cycode.cli.printers.text_printer import TextPrinter
1211
from cycode.cli.printers.utils.code_snippet_syntax import get_code_snippet_syntax
13-
from cycode.cli.printers.utils.detection_data import get_detection_title
12+
from cycode.cli.printers.utils.detection_data import (
13+
get_detection_clickable_cwe_cve,
14+
get_detection_file_path,
15+
get_detection_title,
16+
)
1417
from cycode.cli.printers.utils.detection_ordering.common_ordering import sort_and_group_detections_from_scan_result
1518
from cycode.cli.printers.utils.rich_helpers import get_columns_in_1_to_3_ratio, get_markdown_panel, get_panel
1619

@@ -19,38 +22,28 @@
1922

2023

2124
class RichPrinter(TextPrinter):
25+
MAX_PATH_LENGTH = 60
26+
2227
def print_scan_results(
2328
self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None
2429
) -> None:
2530
if not errors and all(result.issue_detected == 0 for result in local_scan_results):
2631
self.console.print(self.NO_DETECTIONS_MESSAGE)
2732
return
2833

29-
current_file = None
3034
detections, _ = sort_and_group_detections_from_scan_result(local_scan_results)
3135
detections_count = len(detections)
3236
for detection_number, (detection, document) in enumerate(detections, start=1):
33-
if current_file != document.path:
34-
current_file = document.path
35-
self._print_file_header(current_file)
36-
3737
self._print_violation_card(
3838
document,
3939
detection,
4040
detection_number,
4141
detections_count,
4242
)
4343

44+
self.print_scan_results_summary(local_scan_results)
4445
self.print_report_urls_and_errors(local_scan_results, errors)
4546

46-
def _print_file_header(self, file_path: str) -> None:
47-
clickable_path = f'[link=file://{file_path}]{file_path}[/link]'
48-
file_header = Panel(
49-
Text.from_markup(f'[b purple3]:file_folder: File: {clickable_path}[/]', justify='center'),
50-
border_style='dim',
51-
)
52-
self.console.print(file_header)
53-
5447
def _get_details_table(self, detection: 'Detection') -> Table:
5548
details_table = Table(show_header=False, box=None, padding=(0, 1))
5649

@@ -62,15 +55,32 @@ def _get_details_table(self, detection: 'Detection') -> Table:
6255
details_table.add_row('Severity', f'{severity_icon} {SeverityOption(severity).__rich__()}')
6356

6457
detection_details = detection.detection_details
65-
path = Path(detection_details.get('file_name', ''))
66-
details_table.add_row('In file', path.name) # it is name already except for IaC :)
6758

68-
# we do not allow using rich output with SCA; SCA designed to be used with table output
69-
if self.scan_type == consts.IAC_SCAN_TYPE:
70-
details_table.add_row('IaC Provider', detection_details.get('infra_provider'))
71-
elif self.scan_type == consts.SECRET_SCAN_TYPE:
59+
path = str(get_detection_file_path(self.scan_type, detection))
60+
shorten_path = f'...{path[-self.MAX_PATH_LENGTH:]}' if len(path) > self.MAX_PATH_LENGTH else path
61+
details_table.add_row('In file', f'[link=file://{path}]{shorten_path}[/]')
62+
63+
if self.scan_type == consts.SECRET_SCAN_TYPE:
7264
details_table.add_row('Secret SHA', detection_details.get('sha512'))
65+
elif self.scan_type == consts.SCA_SCAN_TYPE:
66+
details_table.add_row('CVEs', get_detection_clickable_cwe_cve(self.scan_type, detection))
67+
details_table.add_row('Package', detection_details.get('package_name'))
68+
details_table.add_row('Version', detection_details.get('package_version'))
69+
70+
is_package_vulnerability = 'alert' in detection_details
71+
if is_package_vulnerability:
72+
details_table.add_row(
73+
'First patched version', detection_details['alert'].get('first_patched_version', 'Not fixed')
74+
)
75+
76+
details_table.add_row('Dependency path', detection_details.get('dependency_paths', 'N/A'))
77+
78+
if not is_package_vulnerability:
79+
details_table.add_row('License', detection_details.get('license'))
80+
elif self.scan_type == consts.IAC_SCAN_TYPE:
81+
details_table.add_row('IaC Provider', detection_details.get('infra_provider'))
7382
elif self.scan_type == consts.SAST_SCAN_TYPE:
83+
details_table.add_row('CWE', get_detection_clickable_cwe_cve(self.scan_type, detection))
7484
details_table.add_row('Subcategory', detection_details.get('category'))
7585
details_table.add_row('Language', ', '.join(detection_details.get('languages', [])))
7686

@@ -105,12 +115,17 @@ def _print_violation_card(
105115
title=':computer: Code Snippet',
106116
)
107117

108-
guidelines_panel = None
109-
guidelines = detection.detection_details.get('remediation_guidelines')
110-
if guidelines:
111-
guidelines_panel = get_markdown_panel(
112-
guidelines,
113-
title=':clipboard: Cycode Guidelines',
118+
is_sca_package_vulnerability = self.scan_type == consts.SCA_SCAN_TYPE and 'alert' in detection.detection_details
119+
if is_sca_package_vulnerability:
120+
summary = detection.detection_details['alert'].get('description')
121+
else:
122+
summary = detection.detection_details.get('description') or detection.message
123+
124+
summary_panel = None
125+
if summary:
126+
summary_panel = get_markdown_panel(
127+
summary,
128+
title=':memo: Summary',
114129
)
115130

116131
custom_guidelines_panel = None
@@ -124,8 +139,8 @@ def _print_violation_card(
124139
navigation = Text(f'Violation {detection_number} of {detections_count}', style='dim', justify='right')
125140

126141
renderables = [navigation, get_columns_in_1_to_3_ratio(details_panel, code_snippet_panel)]
127-
if guidelines_panel:
128-
renderables.append(guidelines_panel)
142+
if summary_panel:
143+
renderables.append(summary_panel)
129144
if custom_guidelines_panel:
130145
renderables.append(custom_guidelines_panel)
131146

cycode/cli/printers/tables/sca_table_printer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
4545
self._print_summary_issues(len(detections), self._get_title(policy_id))
4646
self._print_table(table)
4747

48+
self.print_scan_results_summary(local_scan_results)
4849
self._print_report_urls(local_scan_results, aggregation_report_url)
4950

5051
@staticmethod
@@ -129,7 +130,7 @@ def _enrich_table_with_values(policy_id: str, table: Table, detection: Detection
129130
table.add_cell(LICENSE_COLUMN, detection_details.get('license'))
130131

131132
def _print_summary_issues(self, detections_count: int, title: str) -> None:
132-
self.console.print(f':no_entry: Found {detections_count} issues of type: [b]{title}[/]')
133+
self.console.print(f'[bold]Cycode found {detections_count} violations of type: [cyan]{title}[/]')
133134

134135
@staticmethod
135136
def _extract_detections_per_policy_id(

cycode/cli/printers/tables/table_printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
3737
table.set_group_separator_indexes(group_separator_indexes)
3838

3939
self._print_table(table)
40+
self.print_scan_results_summary(local_scan_results)
4041
self._print_report_urls(local_scan_results, self.ctx.obj.get('aggregation_report_url'))
4142

4243
def _get_table(self) -> Table:

0 commit comments

Comments
 (0)