Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cycode/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Annotated, Optional

import typer
from typer import rich_utils
from typer.completion import install_callback, show_callback

from cycode import __version__
Expand All @@ -18,11 +19,18 @@
from cycode.cyclient.models import UserAgentOptionScheme
from cycode.logger import set_logging_level

# By default, it uses dim style which is hard to read with the combination of color from RICH_HELP
rich_utils.STYLE_ERRORS_SUGGESTION = 'bold'
# By default, it uses blue color which is too dark for some terminals
rich_utils.RICH_HELP = "Try [cyan]'{command_path} {help_option}'[/] for help."


app = typer.Typer(
pretty_exceptions_show_locals=False,
pretty_exceptions_short=True,
context_settings=CLI_CONTEXT_SETTINGS,
rich_markup_mode='rich',
no_args_is_help=True,
add_completion=False, # we add it manually to control the rich help panel
)

Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/apps/ai_remediation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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

# backward compatibility
Expand Down
1 change: 1 addition & 0 deletions cycode/cli/apps/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
app = typer.Typer(
name='auth',
help='Authenticate your machine to associate the CLI with your Cycode account.',
no_args_is_help=True,
)
app.callback(invoke_without_command=True)(auth_command)
app.command(name='check')(check_command)
2 changes: 1 addition & 1 deletion cycode/cli/apps/configure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

app = typer.Typer()
app = typer.Typer(no_args_is_help=True)
app.command(name='configure', short_help='Initial command to configure your CLI client authentication.')(
configure_command
)
2 changes: 1 addition & 1 deletion cycode/cli/apps/ignore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

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

app = typer.Typer()
app = typer.Typer(no_args_is_help=True)
app.command(name='ignore', short_help='Ignores a specific value, path or rule ID.')(ignore_command)
2 changes: 1 addition & 1 deletion cycode/cli/apps/report/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
from cycode.cli.apps.report import sbom
from cycode.cli.apps.report.report_command import report_command

app = typer.Typer(name='report')
app = typer.Typer(name='report', no_args_is_help=True)
app.callback(short_help='Generate report. You`ll need to specify which report type to perform.')(report_command)
app.add_typer(sbom.app)
2 changes: 1 addition & 1 deletion cycode/cli/apps/scan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from cycode.cli.apps.scan.repository.repository_command import repository_command
from cycode.cli.apps.scan.scan_command import scan_command, scan_command_result_callback

app = typer.Typer(name='scan')
app = typer.Typer(name='scan', no_args_is_help=True)

app.callback(
short_help='Scan the content for Secrets, IaC, SCA, and SAST violations.',
Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/apps/status/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
from cycode.cli.apps.status.status_command import status_command
from cycode.cli.apps.status.version_command import version_command

app = typer.Typer()
app = typer.Typer(no_args_is_help=True)
app.command(name='status', short_help='Show the CLI status and exit.')(status_command)
app.command(name='version', hidden=True, short_help='Alias to status command.')(version_command)
4 changes: 2 additions & 2 deletions cycode/cli/cli_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,6 @@ def __rich__(self) -> str:
SeverityOption.INFO.value: ':blue_circle:',
SeverityOption.LOW.value: ':yellow_circle:',
SeverityOption.MEDIUM.value: ':orange_circle:',
SeverityOption.HIGH.value: ':heavy_large_circle:',
SeverityOption.CRITICAL.value: ':red_circle:',
SeverityOption.HIGH.value: ':red_circle:',
SeverityOption.CRITICAL.value: ':exclamation_mark:', # double_exclamation_mark is not red
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ def get_lock_file_name(self) -> str:
def verify_restore_file_already_exist(self, restore_file_path: str) -> bool:
return os.path.isfile(restore_file_path)

def prepare_manifest_file_path_for_command(self, manifest_file_path: str) -> str:
@staticmethod
def prepare_manifest_file_path_for_command(manifest_file_path: str) -> str:
return manifest_file_path.replace(os.sep + NPM_MANIFEST_FILE_NAME, '')
5 changes: 3 additions & 2 deletions cycode/cli/files_collector/sca/sca_code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,15 @@ def try_restore_dependencies(
def add_dependencies_tree_document(
ctx: typer.Context, documents_to_scan: List[Document], is_git_diff: bool = False
) -> None:
documents_to_add: Dict[str, Document] = {}
documents_to_add: Dict[str, Document] = {document.path: document for document in documents_to_scan}
restore_dependencies_list = restore_handlers(ctx, is_git_diff)

for restore_dependencies in restore_dependencies_list:
for document in documents_to_scan:
try_restore_dependencies(ctx, documents_to_add, restore_dependencies, document)

documents_to_scan.extend(list(documents_to_add.values()))
# mutate original list using slice assignment
documents_to_scan[:] = list(documents_to_add.values())


def restore_handlers(ctx: typer.Context, is_git_diff: bool) -> List[BaseRestoreDependencies]:
Expand Down
17 changes: 10 additions & 7 deletions cycode/cli/printers/console_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class ConsolePrinter:
# overrides
'table_sca': ScaTablePrinter,
'text_sca': ScaTablePrinter,
'rich_sca': ScaTablePrinter,
}

def __init__(
Expand All @@ -42,12 +41,7 @@ def __init__(
self.ctx = ctx
self.console = console_override or console
self.console_err = console_err_override or console_err

self.scan_type = self.ctx.obj.get('scan_type')
self.output_type = output_type_override or self.ctx.obj.get('output')
self.aggregation_report_url = self.ctx.obj.get('aggregation_report_url')

self.printer = self._get_scan_printer()

self.console_record = None

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

def _get_scan_printer(self) -> 'PrinterBase':
@property
def scan_type(self) -> str:
return self.ctx.obj.get('scan_type')

@property
def aggregation_report_url(self) -> str:
return self.ctx.obj.get('aggregation_report_url')

@property
def printer(self) -> 'PrinterBase':
printer_class = self._AVAILABLE_PRINTERS.get(self.output_type)

composite_printer = self._AVAILABLE_PRINTERS.get(f'{self.output_type}_{self.scan_type}')
Expand Down
45 changes: 45 additions & 0 deletions cycode/cli/printers/printer_base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import sys
from abc import ABC, abstractmethod
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List, Optional

import typer

from cycode.cli.cli_types import SeverityOption
from cycode.cli.models import CliError, CliResult
from cycode.cyclient.headers import get_correlation_id

Expand Down Expand Up @@ -35,6 +37,18 @@ def __init__(
self.console = console
self.console_err = console_err

@property
def scan_type(self) -> str:
return self.ctx.obj.get('scan_type')

@property
def command_scan_type(self) -> str:
return self.ctx.info_name

@property
def show_secret(self) -> bool:
return self.ctx.obj.get('show_secret', False)

@abstractmethod
def print_scan_results(
self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None
Expand Down Expand Up @@ -64,3 +78,34 @@ def print_exception(self, e: Optional[BaseException] = None) -> None:
self.console_err.print(rich_traceback)

self.console_err.print(f'[red]Correlation ID:[/] {get_correlation_id()}')

def print_scan_results_summary(self, local_scan_results: List['LocalScanResult']) -> None:
"""Print a summary of scan results based on severity levels.

Args:
local_scan_results (List['LocalScanResult']): A list of local scan results containing detections.

The summary includes the count of detections for each severity level
and is displayed in the console in a formatted string.
"""

detections_count = 0
severity_counts = defaultdict(int)
for local_scan_result in local_scan_results:
for document_detections in local_scan_result.document_detections:
for detection in document_detections.detections:
if detection.severity:
detections_count += 1
severity_counts[SeverityOption(detection.severity)] += 1

self.console.print(f'[bold]Cycode found {detections_count} violations[/]', end=': ')

# Example of string: CRITICAL - 6 | HIGH - 0 | MEDIUM - 14 | LOW - 0 | INFO - 0
for index, severity in enumerate(reversed(SeverityOption), start=1):
end = ' | '
if index == len(SeverityOption):
end = '\n'

self.console.print(
SeverityOption.get_member_emoji(severity), severity, '-', severity_counts[severity], end=end
)
73 changes: 44 additions & 29 deletions cycode/cli/printers/rich_printer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional

from rich.console import Group
Expand All @@ -10,7 +9,11 @@
from cycode.cli.cli_types import SeverityOption
from cycode.cli.printers.text_printer import TextPrinter
from cycode.cli.printers.utils.code_snippet_syntax import get_code_snippet_syntax
from cycode.cli.printers.utils.detection_data import get_detection_title
from cycode.cli.printers.utils.detection_data import (
get_detection_clickable_cwe_cve,
get_detection_file_path,
get_detection_title,
)
from cycode.cli.printers.utils.detection_ordering.common_ordering import sort_and_group_detections_from_scan_result
from cycode.cli.printers.utils.rich_helpers import get_columns_in_1_to_3_ratio, get_markdown_panel, get_panel

Expand All @@ -19,38 +22,28 @@


class RichPrinter(TextPrinter):
MAX_PATH_LENGTH = 60

def print_scan_results(
self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None
) -> None:
if not errors and all(result.issue_detected == 0 for result in local_scan_results):
self.console.print(self.NO_DETECTIONS_MESSAGE)
return

current_file = None
detections, _ = sort_and_group_detections_from_scan_result(local_scan_results)
detections_count = len(detections)
for detection_number, (detection, document) in enumerate(detections, start=1):
if current_file != document.path:
current_file = document.path
self._print_file_header(current_file)

self._print_violation_card(
document,
detection,
detection_number,
detections_count,
)

self.print_scan_results_summary(local_scan_results)
self.print_report_urls_and_errors(local_scan_results, errors)

def _print_file_header(self, file_path: str) -> None:
clickable_path = f'[link=file://{file_path}]{file_path}[/link]'
file_header = Panel(
Text.from_markup(f'[b purple3]:file_folder: File: {clickable_path}[/]', justify='center'),
border_style='dim',
)
self.console.print(file_header)

def _get_details_table(self, detection: 'Detection') -> Table:
details_table = Table(show_header=False, box=None, padding=(0, 1))

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

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

# we do not allow using rich output with SCA; SCA designed to be used with table output
if self.scan_type == consts.IAC_SCAN_TYPE:
details_table.add_row('IaC Provider', detection_details.get('infra_provider'))
elif self.scan_type == consts.SECRET_SCAN_TYPE:
path = str(get_detection_file_path(self.scan_type, detection))
shorten_path = f'...{path[-self.MAX_PATH_LENGTH:]}' if len(path) > self.MAX_PATH_LENGTH else path
details_table.add_row('In file', f'[link=file://{path}]{shorten_path}[/]')

if self.scan_type == consts.SECRET_SCAN_TYPE:
details_table.add_row('Secret SHA', detection_details.get('sha512'))
elif self.scan_type == consts.SCA_SCAN_TYPE:
details_table.add_row('CVEs', get_detection_clickable_cwe_cve(self.scan_type, detection))
details_table.add_row('Package', detection_details.get('package_name'))
details_table.add_row('Version', detection_details.get('package_version'))

is_package_vulnerability = 'alert' in detection_details
if is_package_vulnerability:
details_table.add_row(
'First patched version', detection_details['alert'].get('first_patched_version', 'Not fixed')
)

details_table.add_row('Dependency path', detection_details.get('dependency_paths', 'N/A'))

if not is_package_vulnerability:
details_table.add_row('License', detection_details.get('license'))
elif self.scan_type == consts.IAC_SCAN_TYPE:
details_table.add_row('IaC Provider', detection_details.get('infra_provider'))
elif self.scan_type == consts.SAST_SCAN_TYPE:
details_table.add_row('CWE', get_detection_clickable_cwe_cve(self.scan_type, detection))
details_table.add_row('Subcategory', detection_details.get('category'))
details_table.add_row('Language', ', '.join(detection_details.get('languages', [])))

Expand Down Expand Up @@ -105,12 +115,17 @@ def _print_violation_card(
title=':computer: Code Snippet',
)

guidelines_panel = None
guidelines = detection.detection_details.get('remediation_guidelines')
if guidelines:
guidelines_panel = get_markdown_panel(
guidelines,
title=':clipboard: Cycode Guidelines',
is_sca_package_vulnerability = self.scan_type == consts.SCA_SCAN_TYPE and 'alert' in detection.detection_details
if is_sca_package_vulnerability:
summary = detection.detection_details['alert'].get('description')
else:
summary = detection.detection_details.get('description') or detection.message

summary_panel = None
if summary:
summary_panel = get_markdown_panel(
summary,
title=':memo: Summary',
)

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

renderables = [navigation, get_columns_in_1_to_3_ratio(details_panel, code_snippet_panel)]
if guidelines_panel:
renderables.append(guidelines_panel)
if summary_panel:
renderables.append(summary_panel)
if custom_guidelines_panel:
renderables.append(custom_guidelines_panel)

Expand Down
3 changes: 2 additions & 1 deletion cycode/cli/printers/tables/sca_table_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
self._print_summary_issues(len(detections), self._get_title(policy_id))
self._print_table(table)

self.print_scan_results_summary(local_scan_results)
self._print_report_urls(local_scan_results, aggregation_report_url)

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

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

@staticmethod
def _extract_detections_per_policy_id(
Expand Down
1 change: 1 addition & 0 deletions cycode/cli/printers/tables/table_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
table.set_group_separator_indexes(group_separator_indexes)

self._print_table(table)
self.print_scan_results_summary(local_scan_results)
self._print_report_urls(local_scan_results, self.ctx.obj.get('aggregation_report_url'))

def _get_table(self) -> Table:
Expand Down
Loading