Skip to content

Commit 7e49a74

Browse files
authored
CM-63288 cli add error code 2 for scan errors (#440)
1 parent f0a0933 commit 7e49a74

5 files changed

Lines changed: 76 additions & 2 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,14 @@ cycode scan -t sca --stop-on-error path ~/home/git/codebase
802802
803803
This is useful in CI pipelines where a silent failure would produce an incomplete scan result. When `--stop-on-error` is triggered you can either fix the underlying issue or, for SCA restore failures specifically, add `--no-restore` to skip lockfile generation and scan direct dependencies only.
804804
805+
When `--stop-on-error` is used, the CLI distinguishes between scan errors and policy violations via exit codes:
806+
807+
| Exit code | Meaning |
808+
|-----------|---------|
809+
| `0` | Scan completed with no violations |
810+
| `1` | Scan completed and violations were found |
811+
| `2` | Scan aborted due to an error (only when `--stop-on-error` is set) |
812+
805813
### Repository Scan
806814
807815
A repository scan examines an entire local repository for any exposed secrets or insecure misconfigurations. This more holistic scan type looks at everything: the current state of your repository and its commit history. It will look not only for secrets that are currently exposed within the repository but previously deleted secrets as well.

cycode/cli/apps/scan/scan_command.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from cycode.cli.consts import (
1818
ISSUE_DETECTED_STATUS_CODE,
1919
NO_ISSUES_STATUS_CODE,
20+
SCAN_ERROR_STATUS_CODE,
2021
)
2122
from cycode.cli.files_collector.file_excluder import excluder
2223
from cycode.cli.utils import scan_utils
@@ -187,7 +188,9 @@ def scan_command_result_callback(ctx: click.Context, *_, **__) -> None:
187188
raise typer.Exit(0)
188189

189190
exit_code = NO_ISSUES_STATUS_CODE
190-
if scan_utils.is_scan_failed(ctx):
191+
if ctx.obj.get('did_fail') and ctx.obj.get('stop_on_error'):
192+
exit_code = SCAN_ERROR_STATUS_CODE
193+
elif scan_utils.is_scan_failed(ctx):
191194
exit_code = ISSUE_DETECTED_STATUS_CODE
192195

193196
raise typer.Exit(exit_code)

cycode/cli/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@
277277

278278
ISSUE_DETECTED_STATUS_CODE = 1
279279
NO_ISSUES_STATUS_CODE = 0
280+
SCAN_ERROR_STATUS_CODE = 2
280281

281282
LICENSE_COMPLIANCE_POLICY_ID = '8f681450-49e1-4f7e-85b7-0c8fe84b3a35'
282283
PACKAGE_VULNERABILITY_POLICY_ID = '9369d10a-9ac0-48d3-9921-5de7fe9a37a7'

cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ def is_project(self, document: Document) -> bool:
1515
return any(document.path.endswith(ext) for ext in NUGET_PROJECT_FILE_EXTENSIONS)
1616

1717
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
18-
return [['dotnet', 'restore', manifest_file_path, '--use-lock-file', '--verbosity', 'quiet']]
18+
return [
19+
[
20+
'dotnet',
21+
'restore',
22+
manifest_file_path,
23+
'--use-lock-file',
24+
'--verbosity',
25+
'quiet',
26+
'--ignore-failed-sources',
27+
]
28+
]
1929

2030
def get_lock_file_name(self) -> str:
2131
return NUGET_LOCK_FILE_NAME
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import click
2+
import pytest
3+
import typer
4+
5+
from cycode.cli.apps.scan.scan_command import scan_command_result_callback
6+
from cycode.cli.consts import ISSUE_DETECTED_STATUS_CODE, NO_ISSUES_STATUS_CODE, SCAN_ERROR_STATUS_CODE
7+
8+
9+
def _make_ctx(**obj_overrides: object) -> click.Context:
10+
obj = {
11+
'soft_fail': False,
12+
'did_fail': False,
13+
'issue_detected': False,
14+
'stop_on_error': False,
15+
}
16+
obj.update(obj_overrides)
17+
ctx = click.Context(click.Command('scan'))
18+
ctx.obj = obj
19+
return ctx
20+
21+
22+
def _invoke_result_callback(ctx: click.Context) -> int:
23+
with pytest.raises(typer.Exit) as exc_info, ctx:
24+
scan_command_result_callback()
25+
return exc_info.value.exit_code
26+
27+
28+
class TestScanCommandResultCallback:
29+
def test_no_issues_no_errors_exits_zero(self) -> None:
30+
assert _invoke_result_callback(_make_ctx()) == NO_ISSUES_STATUS_CODE
31+
32+
def test_issue_detected_exits_one(self) -> None:
33+
assert _invoke_result_callback(_make_ctx(issue_detected=True)) == ISSUE_DETECTED_STATUS_CODE
34+
35+
def test_did_fail_without_stop_on_error_exits_one(self) -> None:
36+
assert _invoke_result_callback(_make_ctx(did_fail=True)) == ISSUE_DETECTED_STATUS_CODE
37+
38+
def test_did_fail_with_stop_on_error_exits_two(self) -> None:
39+
assert _invoke_result_callback(_make_ctx(did_fail=True, stop_on_error=True)) == SCAN_ERROR_STATUS_CODE
40+
41+
def test_issue_detected_with_stop_on_error_exits_one(self) -> None:
42+
# stop_on_error only affects the error code path, not violations
43+
assert _invoke_result_callback(_make_ctx(issue_detected=True, stop_on_error=True)) == ISSUE_DETECTED_STATUS_CODE
44+
45+
def test_soft_fail_overrides_violations(self) -> None:
46+
assert _invoke_result_callback(_make_ctx(soft_fail=True, issue_detected=True)) == NO_ISSUES_STATUS_CODE
47+
48+
def test_soft_fail_overrides_stop_on_error(self) -> None:
49+
assert (
50+
_invoke_result_callback(_make_ctx(soft_fail=True, did_fail=True, stop_on_error=True))
51+
== NO_ISSUES_STATUS_CODE
52+
)

0 commit comments

Comments
 (0)