Skip to content

Commit 60ebd25

Browse files
omerr-cycodegotbadger
authored andcommitted
CM-61547 add stop-on-error flag to stop file collection on errors
1 parent 9d95dca commit 60ebd25

File tree

9 files changed

+129
-0
lines changed

9 files changed

+129
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ This guide walks you through both installation and usage.
3030
4. [Package Vulnerabilities](#package-vulnerabilities-option)
3131
5. [License Compliance](#license-compliance-option)
3232
6. [Lock Restore](#lock-restore-option)
33+
7. [Stop on Error](#stop-on-error-option)
3334
2. [Repository Scan](#repository-scan)
3435
1. [Branch Option](#branch-option)
3536
3. [Path Scan](#path-scan)
@@ -620,6 +621,7 @@ The Cycode CLI application offers several types of scans so that you can choose
620621
| `--monitor` | When specified, the scan results will be recorded in Cycode. |
621622
| `--cycode-report` | Display a link to the scan report in the Cycode platform in the console output. |
622623
| `--no-restore` | When specified, Cycode will not run the restore command. This will scan direct dependencies ONLY! |
624+
| `--stop-on-error` | Abort the scan if any file collection or dependency restore failure occurs, instead of skipping the failed file and continuing. |
623625
| `--gradle-all-sub-projects` | Run gradle restore command for all sub projects. This should be run from |
624626
| `--maven-settings-file` | For Maven only, allows using a custom [settings.xml](https://maven.apache.org/settings.html) file when scanning for dependencies |
625627
| `--help` | Show options for given command. |
@@ -726,6 +728,18 @@ If a lockfile already exists alongside the manifest, Cycode reads it directly wi
726728
addSbtPlugin("software.purpledragon" % "sbt-dependency-lock" % "1.5.1")
727729
```
728730

731+
#### Stop on Error Option
732+
733+
By default, Cycode continues scanning even if a file cannot be read (e.g. due to a permission error) or a dependency lockfile cannot be generated during an SCA scan. The failed item is skipped with a warning and the scan proceeds with the remaining files.
734+
735+
Use `--stop-on-error` to change this behaviour: the scan aborts immediately on the first such failure and reports the error.
736+
737+
```bash
738+
cycode scan -t sca --stop-on-error path ~/home/git/codebase
739+
```
740+
741+
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.
742+
729743
### Repository Scan
730744

731745
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/code_scanner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def scan_disk_files(ctx: typer.Context, paths: tuple[str, ...]) -> None:
5858
scan_type,
5959
paths,
6060
is_cycodeignore_allowed=is_cycodeignore_allowed_by_scan_config(ctx),
61+
stop_on_error=ctx.obj.get('stop_on_error', False),
6162
)
6263

6364
# Add entrypoint.cycode file at root path to mark the scan root (only for single path that is a directory)

cycode/cli/apps/scan/scan_command.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ def scan_command(
4141
soft_fail: Annotated[
4242
bool, typer.Option('--soft-fail', help='Run the scan without failing; always return a non-error status code.')
4343
] = False,
44+
stop_on_error: Annotated[
45+
bool,
46+
typer.Option(
47+
'--stop-on-error',
48+
help='When specified, stops the scan if any file collection or restore failure occurs.',
49+
),
50+
] = False,
4451
severity_threshold: Annotated[
4552
SeverityOption,
4653
typer.Option(
@@ -131,6 +138,7 @@ def scan_command(
131138

132139
ctx.obj['show_secret'] = show_secret
133140
ctx.obj['soft_fail'] = soft_fail
141+
ctx.obj['stop_on_error'] = stop_on_error
134142
ctx.obj['scan_type'] = scan_type
135143
ctx.obj['sync'] = sync
136144
ctx.obj['severity_threshold'] = severity_threshold

cycode/cli/exceptions/custom_exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ def __str__(self) -> str:
6464
return f'The size of zip to scan is too large, size limit: {self.size_limit}'
6565

6666

67+
class FileCollectionError(CycodeError):
68+
def __init__(self, error_message: str) -> None:
69+
self.error_message = error_message
70+
super().__init__(self.error_message)
71+
72+
def __str__(self) -> str:
73+
return self.error_message
74+
75+
6776
class AuthProcessError(CycodeError):
6877
def __init__(self, error_message: str) -> None:
6978
self.error_message = error_message

cycode/cli/exceptions/handle_scan_errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def handle_scan_exception(ctx: typer.Context, err: Exception, *, return_exceptio
2626
'Please try ignoring irrelevant paths using the `cycode ignore --by-path` command '
2727
'and execute the scan again',
2828
),
29+
custom_exceptions.FileCollectionError: CliError(
30+
soft_fail=False,
31+
code='file_collection_error',
32+
message='File collection failed. '
33+
'Use --no-restore to skip dependency restoration, or fix the underlying issue.',
34+
),
2935
custom_exceptions.TfplanKeyError: CliError(
3036
soft_fail=True,
3137
code='key_error',

cycode/cli/files_collector/path_documents.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from collections.abc import Generator
33
from typing import TYPE_CHECKING
44

5+
from cycode.cli.exceptions.custom_exceptions import FileCollectionError
56
from cycode.cli.files_collector.file_excluder import excluder
67
from cycode.cli.files_collector.iac.tf_content_generator import (
78
generate_tf_content_from_tfplan,
@@ -109,6 +110,7 @@ def get_relevant_documents(
109110
*,
110111
is_git_diff: bool = False,
111112
is_cycodeignore_allowed: bool = True,
113+
stop_on_error: bool = False,
112114
) -> list[Document]:
113115
relevant_files = _get_relevant_files(
114116
progress_bar, progress_bar_section, scan_type, paths, is_cycodeignore_allowed=is_cycodeignore_allowed
@@ -119,6 +121,10 @@ def get_relevant_documents(
119121
progress_bar.update(progress_bar_section)
120122

121123
content = get_file_content(file)
124+
if content is None:
125+
if stop_on_error:
126+
raise FileCollectionError(f'Failed to read file: {file}')
127+
continue
122128
if not content:
123129
continue
124130

cycode/cli/files_collector/sca/sca_file_collector.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import typer
55

66
from cycode.cli import consts
7+
from cycode.cli.exceptions.custom_exceptions import FileCollectionError
78
from cycode.cli.files_collector.repository_documents import get_file_content_from_commit_path
89
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies
910
from cycode.cli.files_collector.sca.go.restore_go_dependencies import RestoreGoDependencies
@@ -116,6 +117,10 @@ def _try_restore_dependencies(
116117
'Error occurred while trying to generate dependencies tree, %s',
117118
{'filename': document.path, 'handler': type(restore_dependencies).__name__},
118119
)
120+
if ctx.obj.get('stop_on_error', False):
121+
raise FileCollectionError(
122+
f'Failed to generate dependencies tree for {document.path} using {type(restore_dependencies).__name__}'
123+
)
119124
return None
120125

121126
if restore_dependencies_document.content is None:

tests/cli/exceptions/test_handle_scan_errors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def ctx() -> typer.Context:
3232
(custom_exceptions.HttpUnauthorizedError('msg', Response()), True),
3333
(custom_exceptions.ZipTooLargeError(1000), True),
3434
(custom_exceptions.TfplanKeyError('msg'), True),
35+
(custom_exceptions.FileCollectionError('Failed to generate dependencies tree for pom.xml'), None),
3536
(git_proxy.get_invalid_git_repository_error()(), None),
3637
],
3738
)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from unittest.mock import MagicMock
2+
3+
import click
4+
import pytest
5+
import typer
6+
7+
from cycode.cli.exceptions.custom_exceptions import FileCollectionError
8+
from cycode.cli.files_collector.sca.sca_file_collector import _try_restore_dependencies
9+
from cycode.cli.models import Document
10+
11+
12+
def _make_ctx(*, stop_on_error: bool = False) -> typer.Context:
13+
ctx = typer.Context(click.Command('path'), obj={'stop_on_error': stop_on_error, 'monitor': False})
14+
ctx.obj['path'] = '/some/path'
15+
return ctx
16+
17+
18+
def _make_handler(*, is_project: bool = True, restore_result: object = None) -> MagicMock:
19+
handler = MagicMock()
20+
handler.is_project.return_value = is_project
21+
handler.restore.return_value = restore_result
22+
return handler
23+
24+
25+
class TestTryRestoreDependencies:
26+
def test_returns_none_when_handler_does_not_match(self) -> None:
27+
ctx = _make_ctx()
28+
doc = Document('pom.xml', '', is_git_diff_format=False)
29+
handler = _make_handler(is_project=False)
30+
31+
result = _try_restore_dependencies(ctx, handler, doc)
32+
33+
assert result is None
34+
handler.restore.assert_not_called()
35+
36+
def test_returns_none_on_restore_failure_without_stop_on_error(self) -> None:
37+
ctx = _make_ctx(stop_on_error=False)
38+
doc = Document('pom.xml', '', is_git_diff_format=False)
39+
handler = _make_handler(is_project=True, restore_result=None)
40+
41+
result = _try_restore_dependencies(ctx, handler, doc)
42+
43+
assert result is None
44+
45+
def test_raises_file_collection_error_on_restore_failure_with_stop_on_error(self) -> None:
46+
ctx = _make_ctx(stop_on_error=True)
47+
doc = Document('pom.xml', '', is_git_diff_format=False)
48+
handler = _make_handler(is_project=True, restore_result=None)
49+
handler.__class__.__name__ = 'RestoreMavenDependencies'
50+
type(handler).__name__ = 'RestoreMavenDependencies'
51+
52+
with pytest.raises(FileCollectionError) as exc_info, ctx:
53+
_try_restore_dependencies(ctx, handler, doc)
54+
55+
assert 'pom.xml' in str(exc_info.value)
56+
57+
def test_returns_document_on_success(self) -> None:
58+
ctx = _make_ctx()
59+
doc = Document('pom.xml', '', is_git_diff_format=False)
60+
restored_doc = Document('pom.xml.lock', 'dep-tree-content', is_git_diff_format=False)
61+
handler = _make_handler(is_project=True, restore_result=restored_doc)
62+
63+
with ctx:
64+
result = _try_restore_dependencies(ctx, handler, doc)
65+
66+
assert result is restored_doc
67+
assert result.content == 'dep-tree-content'
68+
69+
def test_sets_empty_content_when_restore_returns_document_with_none_content(self) -> None:
70+
ctx = _make_ctx()
71+
doc = Document('pom.xml', '', is_git_diff_format=False)
72+
restored_doc = Document('pom.xml.lock', None, is_git_diff_format=False)
73+
handler = _make_handler(is_project=True, restore_result=restored_doc)
74+
75+
with ctx:
76+
result = _try_restore_dependencies(ctx, handler, doc)
77+
78+
assert result is not None
79+
assert result.content == ''

0 commit comments

Comments
 (0)