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
96 changes: 70 additions & 26 deletions cli/code_scanner.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Type, NamedTuple

import click
import json
import logging
Expand All @@ -16,7 +18,7 @@
from cli.models import Document, DocumentDetections, Severity
from cli.ci_integrations import get_commit_range
from cli.consts import *
from cli.config import configuration_manager, config
from cli.config import configuration_manager
from cli.utils.path_utils import is_sub_path, is_binary_file, get_file_size, get_relevant_files_in_path, \
get_path_by_os, get_file_content
from cli.utils.string_utils import get_content_size, is_binary_content
Expand Down Expand Up @@ -817,34 +819,76 @@ def _is_subpath_of_cycode_configuration_folder(filename: str) -> bool:
or filename.endswith(ConfigFileManager.get_config_file_route())


class CliScanError(NamedTuple):
soft_fail: bool
code: str
message: str


CliScanErrors = Dict[Type[Exception], CliScanError]


def _handle_exception(context: click.Context, e: Exception):
context.obj["did_fail"] = True
verbose = context.obj["verbose"]
if verbose:
context.obj['did_fail'] = True

if context.obj['verbose']:
click.secho(f'Error: {traceback.format_exc()}', fg='red', nl=False)
if isinstance(e, (CycodeError, ScanAsyncError)):
click.secho('Cycode was unable to complete this scan. Please try again by executing the `cycode scan` command',
fg='red', nl=False)
context.obj["soft_fail"] = True
elif isinstance(e, HttpUnauthorizedError):
click.secho('Unable to authenticate to Cycode, your token is either invalid or has expired. '
'Please re-generate your token and reconfigure it by running the `cycode configure` command',
fg='red', nl=False)
context.obj["soft_fail"] = True
elif isinstance(e, ZipTooLargeError):
click.secho('The path you attempted to scan exceeds the current maximum scanning size cap (10MB). '
'Please try ignoring irrelevant paths using the ‘cycode ignore --by-path’ '
'command and execute the scan again',
fg='red', nl=False)
context.obj["soft_fail"] = True
elif isinstance(e, InvalidGitRepositoryError):
click.secho('The path you supplied does not correlate to a git repository. Should you still wish to scan '
'this path, use: ‘cycode scan path <path>’',
fg='red', nl=False)
elif isinstance(e, click.ClickException):

# TODO(MarshalX): Create global CLI errors database and move this
errors: CliScanErrors = {
CycodeError: CliScanError(
soft_fail=True,
code='cycode_error',
message='Cycode was unable to complete this scan. '
'Please try again by executing the `cycode scan` command'
),
ScanAsyncError: CliScanError(
soft_fail=True,
code='scan_error',
message='Cycode was unable to complete this scan. '
'Please try again by executing the `cycode scan` command'
),
HttpUnauthorizedError: CliScanError(
soft_fail=True,
code='auth_error',
message='Unable to authenticate to Cycode, your token is either invalid or has expired. '
'Please re-generate your token and reconfigure it by running the `cycode configure` command'
),
ZipTooLargeError: CliScanError(
soft_fail=True,
code='zip_too_large_error',
message='The path you attempted to scan exceeds the current maximum scanning size cap (10MB). '
'Please try ignoring irrelevant paths using the ‘cycode ignore --by-path’ command '
'and execute the scan again'
),
InvalidGitRepositoryError: CliScanError(
soft_fail=False,
code='invalid_git_error',
message='The path you supplied does not correlate to a git repository. '
'Should you still wish to scan this path, use: ‘cycode scan path <path>’'
),
}

if type(e) in errors:
error = errors[type(e)]

if error.soft_fail is True:
context.obj['soft_fail'] = True

return _print_error(context, error)

if isinstance(e, click.ClickException):
raise e
else:
raise click.ClickException(str(e))

raise click.ClickException(str(e))


def _print_error(context: click.Context, error: CliScanError) -> None:
# TODO(MarshalX): Extend functionality of CLI printers and move this
if context.obj['output'] == 'text':
click.secho(error.message, fg='red', nl=False)
elif context.obj['output'] == 'json':
click.echo(json.dumps({'error': error.code, 'message': error.message}, ensure_ascii=False))


def _report_scan_status(context: click.Context, scan_type: str, scan_id: str, scan_completed: bool,
Expand Down
59 changes: 59 additions & 0 deletions tests/cli/test_code_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import click
import pytest
from click import ClickException
from git import InvalidGitRepositoryError

from cli.code_scanner import _handle_exception # noqa
from cli.exceptions import custom_exceptions


@pytest.fixture()
def ctx():
return click.Context(click.Command('path'), obj={'verbose': False, 'output': 'text'})


@pytest.mark.parametrize('exception, expected_soft_fail', [
(custom_exceptions.CycodeError(400, 'msg'), True),
(custom_exceptions.ScanAsyncError('msg'), True),
(custom_exceptions.HttpUnauthorizedError('msg'), True),
(custom_exceptions.ZipTooLargeError(1000), True),
(InvalidGitRepositoryError(), None),
])
def test_handle_exception_soft_fail(
ctx: click.Context, exception: custom_exceptions.CycodeError, expected_soft_fail: bool):
with ctx:
_handle_exception(ctx, exception)

assert ctx.obj.get('did_fail') is True
assert ctx.obj.get('soft_fail') is expected_soft_fail


def test_handle_exception_unhandled_error(ctx: click.Context):
with ctx:
with pytest.raises(ClickException):
_handle_exception(ctx, ValueError('test'))

assert ctx.obj.get('did_fail') is True
assert ctx.obj.get('soft_fail') is None


def test_handle_exception_click_error(ctx: click.Context):
with ctx:
with pytest.raises(ClickException):
_handle_exception(ctx, click.ClickException('test'))

assert ctx.obj.get('did_fail') is True
assert ctx.obj.get('soft_fail') is None


def test_handle_exception_verbose(monkeypatch):
ctx = click.Context(click.Command('path'), obj={'verbose': True, 'output': 'text'})

def mock_secho(msg, *_, **__):
assert 'Error:' in msg

monkeypatch.setattr(click, 'secho', mock_secho)

with ctx:
with pytest.raises(ClickException):
_handle_exception(ctx, ValueError('test'))