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
23 changes: 20 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,34 @@
_SECRET_RICH_HELP_PANEL = 'Secret options'


def _single_value_callback(ctx: typer.Context, param: typer.CallbackParam, value: tuple) -> Any:
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. '
f'Got: {values_str}. '
f'Run a separate command for each value.',
ctx=ctx,
param_hint=param_hint,
)
err.exit_code = 1
raise err
return value[0]


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
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