Skip to content
Open
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
20 changes: 15 additions & 5 deletions cycode/cli/apps/scan/scan_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@
def scan_command(
ctx: typer.Context,
scan_type: Annotated[
ScanTypeOption,
list[ScanTypeOption],
typer.Option(
'--scan-type',
'-t',
help='Specify the type of scan you wish to execute.',
case_sensitive=False,
),
] = ScanTypeOption.SECRET,
] = (ScanTypeOption.SECRET,),
soft_fail: Annotated[
bool, typer.Option('--soft-fail', help='Run the scan without failing; always return a non-error status code.')
] = False,
Expand Down Expand Up @@ -126,6 +126,16 @@ def scan_command(
* `cycode scan commit-history <PATH>`: Scan the commit history of a local Git repository.

"""
if len(scan_type) > 1:
raise typer.BadParameter(
f'Only one scan type can be specified per command. '
f'Got: {", ".join(str(t) for t in scan_type)}. '
f'Run a separate command for each scan type.',
param_hint='-t/--scan-type',
)

resolved_scan_type = scan_type[0]

if export_file and export_type is None:
raise typer.BadParameter(
'Export type must be specified when --export-file is provided.',
Expand All @@ -140,7 +150,7 @@ def scan_command(
ctx.obj['show_secret'] = show_secret
ctx.obj['soft_fail'] = soft_fail
ctx.obj['stop_on_error'] = stop_on_error
ctx.obj['scan_type'] = scan_type
ctx.obj['scan_type'] = resolved_scan_type
ctx.obj['sync'] = sync
ctx.obj['severity_threshold'] = severity_threshold
ctx.obj['monitor'] = monitor
Expand All @@ -158,9 +168,9 @@ def scan_command(
# Get remote URL from current working directory
remote_url = _try_get_git_remote_url(os.getcwd())

remote_scan_config = scan_client.get_scan_configuration_safe(scan_type, remote_url)
remote_scan_config = scan_client.get_scan_configuration_safe(resolved_scan_type, remote_url)
if remote_scan_config:
excluder.apply_scan_config(str(scan_type), remote_scan_config)
excluder.apply_scan_config(str(resolved_scan_type), remote_scan_config)

ctx.obj['scan_config'] = remote_scan_config

Expand Down
16 changes: 16 additions & 0 deletions tests/cli/commands/scan/test_scan_command.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import click
import pytest
import typer
from typer.testing import CliRunner

from cycode.cli.app import app
from cycode.cli.apps.scan.scan_command import scan_command_result_callback
from cycode.cli.consts import ISSUE_DETECTED_STATUS_CODE, NO_ISSUES_STATUS_CODE, SCAN_ERROR_STATUS_CODE

Expand All @@ -25,6 +27,20 @@ def _invoke_result_callback(ctx: click.Context) -> int:
return exc_info.value.exit_code


class TestScanCommand:
def test_multiple_scan_types_rejected(self) -> None:
result = CliRunner().invoke(app, ['scan', '-t', 'iac', '-t', 'sast', 'path', '.'])
assert result.exit_code == 2
assert '-t/--scan-type' in result.output
assert 'iac' in result.output
assert 'sast' in result.output

def test_single_scan_type_accepted(self) -> None:
result = CliRunner().invoke(app, ['scan', '-t', 'iac', '--help'])
assert result.exit_code == 0
assert 'Error' not in result.output


class TestScanCommandResultCallback:
def test_no_issues_no_errors_exits_zero(self) -> None:
assert _invoke_result_callback(_make_ctx()) == NO_ISSUES_STATUS_CODE
Expand Down