Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 4 additions & 0 deletions cycode/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from cycode.cli.consts import PROGRAM_NAME
from cycode.cli.main import app

app(prog_name=PROGRAM_NAME)
3 changes: 0 additions & 3 deletions cycode/cli/__main__.py

This file was deleted.

44 changes: 17 additions & 27 deletions cycode/cli/app.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import logging
from pathlib import Path
from typing import Annotated, Optional

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

from cycode import __version__
from cycode.cli.apps import ai_remediation, auth, configure, ignore, report, scan, status
from cycode.cli.cli_types import ExportTypeOption, OutputTypeOption
from cycode.cli.cli_types import OutputTypeOption
from cycode.cli.consts import CLI_CONTEXT_SETTINGS
from cycode.cli.printers import ConsolePrinter
from cycode.cli.user_settings.configuration_manager import ConfigurationManager
Expand All @@ -24,14 +24,10 @@
# 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."

completion_init() # DO NOT TOUCH; this is required for the completion to work properly

_cycode_cli_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md'
_cycode_cli_epilog = f"""[bold]Documentation[/]



For more details and advanced usage, visit: [link={_cycode_cli_docs}]{_cycode_cli_docs}[/link]
"""
_cycode_cli_epilog = f'[bold]Documentation:[/] [link={_cycode_cli_docs}]{_cycode_cli_docs}[/link]'

app = typer.Typer(
pretty_exceptions_show_locals=False,
Expand Down Expand Up @@ -69,8 +65,8 @@ def export_if_needed_on_close(ctx: typer.Context) -> None:
printer.export()


_AUTH_RICH_HELP_PANEL = 'Authentication options'
_COMPLETION_RICH_HELP_PANEL = 'Completion options'
_EXPORT_RICH_HELP_PANEL = 'Export options'


@app.callback()
Expand All @@ -90,25 +86,18 @@ def app_callback(
Optional[str],
typer.Option(hidden=True, help='Characteristic JSON object that lets servers identify the application.'),
] = None,
export_type: Annotated[
ExportTypeOption,
client_secret: Annotated[
Optional[str],
typer.Option(
'--export-type',
case_sensitive=False,
help='Specify the export type. '
'HTML and SVG will export terminal output and rely on --output option. '
'JSON always exports JSON.',
rich_help_panel=_EXPORT_RICH_HELP_PANEL,
help='Specify a Cycode client secret for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = ExportTypeOption.JSON,
export_file: Annotated[
Optional[Path],
] = None,
client_id: Annotated[
Optional[str],
typer.Option(
'--export-file',
help='Export file. Path to the file where the export will be saved. ',
dir_okay=False,
writable=True,
rich_help_panel=_EXPORT_RICH_HELP_PANEL,
help='Specify a Cycode client ID for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
_: Annotated[
Expand Down Expand Up @@ -150,10 +139,11 @@ def app_callback(
if output == OutputTypeOption.JSON:
no_progress_meter = True

ctx.obj['client_id'] = client_id
ctx.obj['client_secret'] = client_secret

ctx.obj['progress_bar'] = get_progress_bar(hidden=no_progress_meter, sections=SCAN_PROGRESS_BAR_SECTIONS)

ctx.obj['export_type'] = export_type
ctx.obj['export_file'] = export_file
ctx.obj['console_printer'] = ConsolePrinter(ctx)
ctx.call_on_close(lambda: export_if_needed_on_close(ctx))

Expand Down
6 changes: 3 additions & 3 deletions cycode/cli/apps/ai_remediation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

app = typer.Typer()

_ai_remediation_epilog = """
Note: AI remediation suggestions are generated automatically and should be reviewed before applying.
"""
_ai_remediation_epilog = (
'Note: AI remediation suggestions are generated automatically and should be reviewed before applying.'
)

app.command(
name='ai-remediation',
Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/apps/ai_remediation/ai_remediation_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def ai_remediation_command(
* `cycode ai-remediation <detection_id>`: View remediation guidance
* `cycode ai-remediation <detection_id> --fix`: Apply suggested fixes
"""
client = get_scan_cycode_client()
client = get_scan_cycode_client(ctx)

try:
remediation_markdown = client.get_ai_remediation(detection_id)
Expand Down
7 changes: 1 addition & 6 deletions cycode/cli/apps/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@
from cycode.cli.apps.auth.auth_command import auth_command

_auth_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#using-the-auth-command'
_auth_command_epilog = f"""[bold]Documentation[/]



For more details and advanced usage, visit: [link={_auth_command_docs}]{_auth_command_docs}[/link]
"""
_auth_command_epilog = f'[bold]Documentation:[/] [link={_auth_command_docs}]{_auth_command_docs}[/link]'

app = typer.Typer(no_args_is_help=False)
app.command(name='auth', epilog=_auth_command_epilog, short_help='Authenticate your machine with Cycode.')(auth_command)
5 changes: 4 additions & 1 deletion cycode/cli/apps/auth/auth_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
printer = ctx.obj.get('console_printer')

client_id, client_secret = CredentialsManager().get_credentials()
client_id, client_secret = ctx.obj.get('client_id'), ctx.obj.get('client_secret')
if not client_id or not client_secret:
client_id, client_secret = CredentialsManager().get_credentials()

if not client_id or not client_secret:
return None

Expand Down
7 changes: 1 addition & 6 deletions cycode/cli/apps/configure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@
from cycode.cli.apps.configure.configure_command import configure_command

_configure_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#using-the-configure-command'
_configure_command_epilog = f"""[bold]Documentation[/]



For more details and advanced usage, visit: [link={_configure_command_docs}]{_configure_command_docs}[/link]
"""
_configure_command_epilog = f'[bold]Documentation:[/] [link={_configure_command_docs}]{_configure_command_docs}[/link]'


app = typer.Typer(no_args_is_help=True)
Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/apps/report/sbom/path/path_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def path_command(
) -> None:
add_breadcrumb('path')

client = get_report_cycode_client()
client = get_report_cycode_client(ctx)
report_parameters = ctx.obj['report_parameters']
output_format = report_parameters.output_format
output_file = ctx.obj['output_file']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def repository_url_command(
progress_bar.start()
progress_bar.set_section_length(SbomReportProgressBarSection.PREPARE_LOCAL_FILES)

client = get_report_cycode_client()
client = get_report_cycode_client(ctx)
report_parameters = ctx.obj['report_parameters']
output_file = ctx.obj['output_file']
output_format = report_parameters.output_format
Expand Down
7 changes: 1 addition & 6 deletions cycode/cli/apps/scan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@
app = typer.Typer(name='scan', no_args_is_help=True)

_scan_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#scan-command'
_scan_command_epilog = f"""[bold]Documentation[/]



For more details and advanced usage, visit: [link={_scan_command_docs}]{_scan_command_docs}[/link]
"""
_scan_command_epilog = f'[bold]Documentation:[/] [link={_scan_command_docs}]{_scan_command_docs}[/link]'

app.callback(
short_help='Scan the content for Secrets, IaC, SCA, and SAST violations.',
Expand Down
1 change: 1 addition & 0 deletions cycode/cli/apps/scan/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ def print_results(
ctx: typer.Context, local_scan_results: list[LocalScanResult], errors: Optional[dict[str, 'CliError']] = None
) -> None:
printer = ctx.obj.get('console_printer')
printer.update_ctx(ctx)
printer.print_scan_results(local_scan_results, errors)


Expand Down
51 changes: 33 additions & 18 deletions cycode/cli/apps/scan/scan_command.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from pathlib import Path
from typing import Annotated, Optional

import click
import typer

from cycode.cli.cli_types import ScanTypeOption, ScaScanTypeOption, SeverityOption
from cycode.cli.cli_types import ExportTypeOption, ScanTypeOption, ScaScanTypeOption, SeverityOption
from cycode.cli.consts import (
ISSUE_DETECTED_STATUS_CODE,
NO_ISSUES_STATUS_CODE,
Expand All @@ -12,8 +13,9 @@
from cycode.cli.utils.get_api_client import get_scan_cycode_client
from cycode.cli.utils.sentry import add_breadcrumb

_AUTH_RICH_HELP_PANEL = 'Authentication options'
_EXPORT_RICH_HELP_PANEL = 'Export options'
_SCA_RICH_HELP_PANEL = 'SCA options'
_SECRET_RICH_HELP_PANEL = 'Secret options'


def scan_command(
Expand All @@ -27,21 +29,9 @@ def scan_command(
case_sensitive=False,
),
] = ScanTypeOption.SECRET,
client_secret: Annotated[
Optional[str],
typer.Option(
help='Specify a Cycode client secret for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
client_id: Annotated[
Optional[str],
typer.Option(
help='Specify a Cycode client ID for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
show_secret: Annotated[bool, typer.Option('--show-secret', help='Show Secrets in plain text.')] = False,
show_secret: Annotated[
bool, typer.Option('--show-secret', help='Show Secrets in plain text.', rich_help_panel=_SECRET_RICH_HELP_PANEL)
] = False,
soft_fail: Annotated[
bool, typer.Option('--soft-fail', help='Run the scan without failing; always return a non-error status code.')
] = False,
Expand All @@ -66,6 +56,27 @@ def scan_command(
'A link to the report will be displayed in the console output.',
),
] = False,
export_type: Annotated[
ExportTypeOption,
typer.Option(
'--export-type',
case_sensitive=False,
help='Specify the export type. '
'HTML and SVG will export terminal output and rely on --output option. '
'JSON always exports JSON.',
rich_help_panel=_EXPORT_RICH_HELP_PANEL,
),
] = ExportTypeOption.JSON,
export_file: Annotated[
Optional[Path],
typer.Option(
'--export-file',
help='Export file. Path to the file where the export will be saved. ',
dir_okay=False,
writable=True,
rich_help_panel=_EXPORT_RICH_HELP_PANEL,
),
] = None,
sca_scan: Annotated[
list[ScaScanTypeOption],
typer.Option(
Expand Down Expand Up @@ -117,13 +128,17 @@ def scan_command(

ctx.obj['show_secret'] = show_secret
ctx.obj['soft_fail'] = soft_fail
ctx.obj['client'] = get_scan_cycode_client(client_id, client_secret, not ctx.obj['show_secret'])
ctx.obj['client'] = get_scan_cycode_client(ctx)
ctx.obj['scan_type'] = scan_type
ctx.obj['sync'] = sync
ctx.obj['severity_threshold'] = severity_threshold
ctx.obj['monitor'] = monitor
ctx.obj['report'] = report

if export_file:
console_printer = ctx.obj['console_printer']
console_printer.enable_recording(export_type, export_file)

_ = no_restore, gradle_all_sub_projects # they are actually used; via ctx.params

_sca_scan_to_context(ctx, sca_scan)
Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/apps/status/get_cli_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def get_cli_status(ctx: 'Context') -> CliStatus:
supported_modules_status = CliSupportedModulesStatus()
if is_authenticated:
try:
client = get_scan_cycode_client()
client = get_scan_cycode_client(ctx)
supported_modules_preferences = client.get_supported_modules_preferences()

supported_modules_status.secret_scanning = supported_modules_preferences.secret_scanning
Expand Down
43 changes: 27 additions & 16 deletions cycode/cli/printers/console_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from cycode.cli.printers.text_printer import TextPrinter

if TYPE_CHECKING:
from pathlib import Path

from cycode.cli.models import LocalScanResult
from cycode.cli.printers.tables.table_printer_base import PrinterBase

Expand Down Expand Up @@ -43,17 +45,9 @@ def __init__(
self.console_err = console_err_override or console_err
self.output_type = output_type_override or self.ctx.obj.get('output')

self.console_record = None

self.export_type = self.ctx.obj.get('export_type')
self.export_file = self.ctx.obj.get('export_file')
if console_override is None and self.export_type and self.export_file:
self.console_record = ConsolePrinter(
ctx,
console_override=Console(record=True, file=io.StringIO()),
console_err_override=Console(stderr=True, record=True, file=io.StringIO()),
output_type_override='json' if self.export_type == 'json' else self.output_type,
)
self.export_type: Optional[str] = None
self.export_file: Optional[Path] = None
self.console_record: Optional[ConsolePrinter] = None

@property
def scan_type(self) -> str:
Expand All @@ -76,6 +70,21 @@ def printer(self) -> 'PrinterBase':

return printer_class(self.ctx, self.console, self.console_err)

def update_ctx(self, ctx: 'typer.Context') -> None:
self.ctx = ctx

def enable_recording(self, export_type: str, export_file: 'Path') -> None:
if self.console_record is None:
self.export_file = export_file
self.export_type = export_type

self.console_record = ConsolePrinter(
self.ctx,
console_override=Console(record=True, file=io.StringIO()),
console_err_override=Console(stderr=True, record=True, file=io.StringIO()),
output_type_override='json' if self.export_type == 'json' else self.output_type,
)

def print_scan_results(
self,
local_scan_results: list['LocalScanResult'],
Expand Down Expand Up @@ -106,16 +115,18 @@ def export(self) -> None:
if self.console_record is None:
raise CycodeError('Console recording was not enabled. Cannot export.')

if not self.export_file.suffix:
export_file = self.export_file
if not export_file.suffix:
# resolve file extension based on the export type if not provided in the file name
self.export_file = self.export_file.with_suffix(f'.{self.export_type.lower()}')
export_file = export_file.with_suffix(f'.{self.export_type.lower()}')

export_file = str(export_file)
if self.export_type is ExportTypeOption.HTML:
self.console_record.console.save_html(self.export_file)
self.console_record.console.save_html(export_file)
elif self.export_type is ExportTypeOption.SVG:
self.console_record.console.save_svg(self.export_file, title=consts.APP_NAME)
self.console_record.console.save_svg(export_file, title=consts.APP_NAME)
elif self.export_type is ExportTypeOption.JSON:
with open(self.export_file, 'w', encoding='UTF-8') as f:
with open(export_file, 'w', encoding='UTF-8') as f:
self.console_record.console.file.seek(0)
f.write(self.console_record.console.file.read())
else:
Expand Down
Loading