Skip to content
Open
24 changes: 21 additions & 3 deletions cycode/cli/apps/scan/scan_command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from pathlib import Path
from typing import Annotated, Optional
from typing import Annotated, Any, Optional

import click
import typer
Expand Down Expand Up @@ -28,17 +28,32 @@
_SECRET_RICH_HELP_PANEL = 'Secret options'


def _single_value_callback(ctx: typer.Context, param: typer.CallbackParam, value: list) -> list:
if len(value) > 1:
values_str = ', '.join(str(v) for v in value)
param_hint = '/'.join(sorted(param.opts, key=len))
err = typer.BadParameter(
f'Only one value can be specified per command. Got: {values_str}. Run a separate command for each value.',
ctx=ctx,
param_hint=param_hint,
)
err.exit_code = 1
raise err
return value


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,
callback=_single_value_callback,
),
] = 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 @@ -137,6 +152,9 @@ def scan_command(
param_hint='--export-file',
)

# _single_value_callback validated exactly one value was provided; unwrap from list
scan_type = scan_type[0]

ctx.obj['show_secret'] = show_secret
ctx.obj['soft_fail'] = soft_fail
ctx.obj['stop_on_error'] = stop_on_error
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 == 1
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