Skip to content

Commit 657c7fc

Browse files
authored
CM-22718 - Support scan in batches with progress bar (#129)
1 parent c7f5781 commit 657c7fc

25 files changed

+1131
-550
lines changed

cycode/cli/auth/auth_command.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ def authorization_check(context: click.Context):
4848
return printer.print_result(passed_auth_check_res)
4949
except (NetworkError, HttpUnauthorizedError):
5050
if context.obj['verbose']:
51-
click.secho(f'Error: {traceback.format_exc()}', fg='red', nl=False)
51+
click.secho(f'Error: {traceback.format_exc()}', fg='red')
5252

5353
return printer.print_result(failed_auth_check_res)
5454

5555

5656
def _handle_exception(context: click.Context, e: Exception):
5757
if context.obj['verbose']:
58-
click.secho(f'Error: {traceback.format_exc()}', fg='red', nl=False)
58+
click.secho(f'Error: {traceback.format_exc()}', fg='red')
5959

6060
errors: CliErrors = {
6161
AuthProcessError: CliError(

cycode/cli/code_scanner.py

Lines changed: 609 additions & 338 deletions
Large diffs are not rendered by default.

cycode/cli/consts.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@
8484
# 200MB in bytes (in binary)
8585
SCA_ZIP_MAX_SIZE_LIMIT_IN_BYTES = 209715200
8686

87+
# scan in batches
88+
SCAN_BATCH_MAX_SIZE_IN_BYTES = 9 * 1024 * 1024
89+
SCAN_BATCH_MAX_FILES_COUNT = 1000
90+
# if we increase this values, the server doesn't allow connecting (ConnectionError)
91+
SCAN_BATCH_MAX_PARALLEL_SCANS = 5
92+
SCAN_BATCH_SCANS_PER_CPU = 1
93+
8794
# scan with polling
8895
SCAN_POLLING_WAIT_INTERVAL_IN_SECONDS = 5
8996
DEFAULT_SCAN_POLLING_TIMEOUT_IN_SECONDS = 3600
@@ -93,8 +100,6 @@
93100
DEFAULT_SCA_PRE_COMMIT_TIMEOUT_IN_SECONDS = 600
94101
SCA_PRE_COMMIT_TIMEOUT_IN_SECONDS_ENV_VAR_NAME = 'SCA_PRE_COMMIT_TIMEOUT_IN_SECONDS'
95102

96-
PROGRESS_UPDATE_COMMITS_INTERVAL = 100
97-
98103
# pre receive scan
99104
PRE_RECEIVE_MAX_COMMITS_TO_SCAN_COUNT_ENV_VAR_NAME = 'PRE_RECEIVE_MAX_COMMITS_TO_SCAN_COUNT'
100105
DEFAULT_PRE_RECEIVE_MAX_COMMITS_TO_SCAN_COUNT = 50
@@ -124,6 +129,9 @@
124129
SKIP_SCAN_FLAG = 'skip-cycode-scan'
125130
VERBOSE_SCAN_FLAG = 'verbose'
126131

132+
ISSUE_DETECTED_STATUS_CODE = 1
133+
NO_ISSUES_STATUS_CODE = 0
134+
127135
LICENSE_COMPLIANCE_POLICY_ID = '8f681450-49e1-4f7e-85b7-0c8fe84b3a35'
128136
PACKAGE_VULNERABILITY_POLICY_ID = '9369d10a-9ac0-48d3-9921-5de7fe9a37a7'
129137

cycode/cli/main.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import click
44
import sys
55

6-
from typing import List, Optional
6+
from typing import List, Optional, TYPE_CHECKING
77

88
from cycode import __version__
9+
from cycode.cli.consts import NO_ISSUES_STATUS_CODE, ISSUE_DETECTED_STATUS_CODE
910
from cycode.cli.models import Severity
1011
from cycode.cli.config import config
1112
from cycode.cli import code_scanner
@@ -14,14 +15,17 @@
1415
from cycode.cli.user_settings.user_settings_commands import set_credentials, add_exclusions
1516
from cycode.cli.auth.auth_command import authenticate
1617
from cycode.cli.utils import scan_utils
18+
from cycode.cli.utils.progress_bar import get_progress_bar
1719
from cycode.cyclient import logger
20+
from cycode.cli.utils.progress_bar import logger as progress_bar_logger
1821
from cycode.cyclient.cycode_client_base import CycodeClientBase
1922
from cycode.cyclient.models import UserAgentOptionScheme
2023
from cycode.cyclient.scan_config.scan_config_creator import create_scan_client
2124

25+
if TYPE_CHECKING:
26+
from cycode.cyclient.scan_client import ScanClient
27+
2228
CONTEXT = dict()
23-
ISSUE_DETECTED_STATUS_CODE = 1
24-
NO_ISSUES_STATUS_CODE = 0
2529

2630

2731
@click.group(
@@ -106,25 +110,41 @@ def code_scan(context: click.Context, scan_type, client_id, secret, show_secret,
106110
context.obj["soft_fail"] = config["soft_fail"]
107111

108112
context.obj["scan_type"] = scan_type
113+
114+
# save backward compatability with old style command
109115
if output is not None:
110-
# save backward compatability with old style command
111116
context.obj["output"] = output
117+
if output == "json":
118+
context.obj["no_progress_meter"] = True
119+
112120
context.obj["client"] = get_cycode_client(client_id, secret)
113121
context.obj["severity_threshold"] = severity_threshold
114122
context.obj["monitor"] = monitor
115123
context.obj["report"] = report
124+
116125
_sca_scan_to_context(context, sca_scan)
117126

127+
context.obj["progress_bar"] = get_progress_bar(hidden=context.obj["no_progress_meter"])
128+
context.obj["progress_bar"].start()
129+
118130
return 1
119131

120132

121133
@code_scan.result_callback()
122134
@click.pass_context
123-
def finalize(context: click.Context, *args, **kwargs):
124-
if context.obj["soft_fail"]:
135+
def finalize(context: click.Context, *_, **__):
136+
progress_bar = context.obj.get('progress_bar')
137+
if progress_bar:
138+
progress_bar.stop()
139+
140+
if context.obj['soft_fail']:
125141
sys.exit(0)
126142

127-
sys.exit(ISSUE_DETECTED_STATUS_CODE if _should_fail_scan(context) else NO_ISSUES_STATUS_CODE)
143+
exit_code = NO_ISSUES_STATUS_CODE
144+
if _should_fail_scan(context):
145+
exit_code = ISSUE_DETECTED_STATUS_CODE
146+
147+
sys.exit(exit_code)
128148

129149

130150
@click.group(
@@ -139,6 +159,9 @@ def finalize(context: click.Context, *args, **kwargs):
139159
@click.option(
140160
"--verbose", "-v", is_flag=True, default=False, help="Show detailed logs",
141161
)
162+
@click.option(
163+
'--no-progress-meter', is_flag=True, default=False, help='Do not show the progress meter',
164+
)
142165
@click.option(
143166
'--output',
144167
default='text',
@@ -153,23 +176,29 @@ def finalize(context: click.Context, *args, **kwargs):
153176
)
154177
@click.version_option(__version__, prog_name="cycode")
155178
@click.pass_context
156-
def main_cli(context: click.Context, verbose: bool, output: str, user_agent: Optional[str]):
179+
def main_cli(context: click.Context, verbose: bool, no_progress_meter: bool, output: str, user_agent: Optional[str]):
157180
context.ensure_object(dict)
158181
configuration_manager = ConfigurationManager()
159182

160183
verbose = verbose or configuration_manager.get_verbose_flag()
161184
context.obj['verbose'] = verbose
185+
# TODO(MarshalX): rework setting the log level for loggers
162186
log_level = logging.DEBUG if verbose else logging.INFO
163187
logger.setLevel(log_level)
188+
progress_bar_logger.setLevel(log_level)
164189

165190
context.obj['output'] = output
191+
if output == 'json':
192+
no_progress_meter = True
193+
194+
context.obj['no_progress_meter'] = no_progress_meter
166195

167196
if user_agent:
168197
user_agent_option = UserAgentOptionScheme().loads(user_agent)
169198
CycodeClientBase.enrich_user_agent(user_agent_option.user_agent_suffix)
170199

171200

172-
def get_cycode_client(client_id, client_secret):
201+
def get_cycode_client(client_id: str, client_secret: str) -> 'ScanClient':
173202
if not client_id or not client_secret:
174203
client_id, client_secret = _get_configured_credentials()
175204
if not client_id:

cycode/cli/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from enum import Enum
2-
from typing import List, NamedTuple, Dict, Type
2+
from typing import List, NamedTuple, Dict, Type, Optional
33

44
from cycode.cyclient.models import Detection
55

@@ -58,3 +58,12 @@ class CliError(NamedTuple):
5858
class CliResult(NamedTuple):
5959
success: bool
6060
message: str
61+
62+
63+
class LocalScanResult(NamedTuple):
64+
scan_id: str
65+
report_url: Optional[str]
66+
document_detections: List[DocumentDetections]
67+
issue_detected: bool
68+
detections_count: int
69+
relevant_detections_count: int

cycode/cli/printers/base_printer.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from abc import ABC, abstractmethod
2-
from typing import List
2+
from typing import List, TYPE_CHECKING
33

44
import click
55

6-
from cycode.cli.models import DocumentDetections, CliResult, CliError
6+
from cycode.cli.models import CliResult, CliError
7+
8+
if TYPE_CHECKING:
9+
from cycode.cli.models import LocalScanResult
710

811

912
class BasePrinter(ABC):
@@ -15,7 +18,7 @@ def __init__(self, context: click.Context):
1518
self.context = context
1619

1720
@abstractmethod
18-
def print_scan_results(self, results: List[DocumentDetections]) -> None:
21+
def print_scan_results(self, local_scan_results: List['LocalScanResult']) -> None:
1922
pass
2023

2124
@abstractmethod
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import abc
2-
from typing import List
2+
from typing import List, TYPE_CHECKING
33

44
import click
55

66
from cycode.cli.printers.text_printer import TextPrinter
7-
from cycode.cli.models import DocumentDetections, CliError, CliResult
7+
from cycode.cli.models import CliError, CliResult
88
from cycode.cli.printers.base_printer import BasePrinter
99

10+
if TYPE_CHECKING:
11+
from cycode.cli.models import LocalScanResult
12+
1013

1114
class BaseTablePrinter(BasePrinter, abc.ABC):
1215
def __init__(self, context: click.Context):
1316
super().__init__(context)
1417
self.context = context
15-
self.scan_id: str = context.obj.get('scan_id')
1618
self.scan_type: str = context.obj.get('scan_type')
1719
self.show_secret: bool = context.obj.get('show_secret', False)
1820

@@ -22,22 +24,16 @@ def print_result(self, result: CliResult) -> None:
2224
def print_error(self, error: CliError) -> None:
2325
TextPrinter(self.context).print_error(error)
2426

25-
def print_scan_results(self, results: List[DocumentDetections]):
26-
click.secho(f'Scan Results: (scan_id: {self.scan_id})')
27-
28-
if not results:
27+
def print_scan_results(self, local_scan_results: List['LocalScanResult']):
28+
if all(result.issue_detected == 0 for result in local_scan_results):
2929
click.secho('Good job! No issues were found!!! 👏👏👏', fg=self.GREEN_COLOR_NAME)
3030
return
3131

32-
self._print_results(results)
33-
34-
report_url = self.context.obj.get('report_url')
35-
if report_url:
36-
click.secho(f'Report URL: {report_url}')
32+
self._print_results(local_scan_results)
3733

3834
def _is_git_repository(self) -> bool:
3935
return self.context.obj.get('remote_url') is not None
4036

4137
@abc.abstractmethod
42-
def _print_results(self, results: List[DocumentDetections]) -> None:
38+
def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
4339
raise NotImplementedError

cycode/cli/printers/console_printer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
from typing import List, TYPE_CHECKING
33

44
from cycode.cli.exceptions.custom_exceptions import CycodeError
5-
from cycode.cli.models import DocumentDetections, CliResult, CliError
5+
from cycode.cli.models import CliResult, CliError
66
from cycode.cli.printers.table_printer import TablePrinter
77
from cycode.cli.printers.sca_table_printer import SCATablePrinter
88
from cycode.cli.printers.json_printer import JsonPrinter
99
from cycode.cli.printers.text_printer import TextPrinter
1010

1111
if TYPE_CHECKING:
12+
from cycode.cli.models import LocalScanResult
1213
from cycode.cli.printers.base_printer import BasePrinter
1314

1415

@@ -31,9 +32,9 @@ def __init__(self, context: click.Context):
3132
if self._printer_class is None:
3233
raise CycodeError(f'"{self.output_type}" output type is not supported.')
3334

34-
def print_scan_results(self, detections_results_list: List[DocumentDetections]) -> None:
35+
def print_scan_results(self, local_scan_results: List['LocalScanResult']) -> None:
3536
printer = self._get_scan_printer()
36-
printer.print_scan_results(detections_results_list)
37+
printer.print_scan_results(local_scan_results)
3738

3839
def _get_scan_printer(self) -> 'BasePrinter':
3940
printer_class = self._printer_class

cycode/cli/printers/json_printer.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import json
2-
from typing import List
2+
from typing import List, TYPE_CHECKING
33

44
import click
55

6-
from cycode.cli.models import DocumentDetections, CliResult, CliError
6+
from cycode.cli.models import CliResult, CliError
77
from cycode.cli.printers.base_printer import BasePrinter
88
from cycode.cyclient.models import DetectionSchema
99

10+
if TYPE_CHECKING:
11+
from cycode.cli.models import LocalScanResult
1012

11-
class JsonPrinter(BasePrinter):
12-
def __init__(self, context: click.Context):
13-
super().__init__(context)
14-
self.scan_id = context.obj.get('scan_id')
1513

14+
class JsonPrinter(BasePrinter):
1615
def print_result(self, result: CliResult) -> None:
1716
result = {
1817
'result': result.success,
@@ -29,18 +28,19 @@ def print_error(self, error: CliError) -> None:
2928

3029
click.secho(self.get_data_json(result))
3130

32-
def print_scan_results(self, results: List[DocumentDetections]) -> None:
31+
def print_scan_results(self, local_scan_results: List['LocalScanResult']) -> None:
3332
detections = []
34-
for result in results:
35-
detections.extend(result.detections)
33+
for local_scan_result in local_scan_results:
34+
for document_detections in local_scan_result.document_detections:
35+
detections.extend(document_detections.detections)
3636

3737
detections_dict = DetectionSchema(many=True).dump(detections)
3838

3939
click.secho(self._get_json_scan_result(detections_dict))
4040

4141
def _get_json_scan_result(self, detections: dict) -> str:
4242
result = {
43-
'scan_id': str(self.scan_id),
43+
'scan_id': 'DEPRECATED', # FIXME(MarshalX): we need change JSON struct to support multiple scan results
4444
'detections': detections
4545
}
4646

0 commit comments

Comments
 (0)