Skip to content

Commit 3f472b0

Browse files
authored
CM-29446 - Performance improvements for SCA (new sync flow) (#209)
1 parent 8e08dc7 commit 3f472b0

File tree

6 files changed

+106
-13
lines changed

6 files changed

+106
-13
lines changed

cycode/cli/commands/scan/code_scanner.py

+40-4
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,21 @@ def set_issue_detected_by_scan_results(context: click.Context, scan_results: Lis
9898
set_issue_detected(context, any(scan_result.issue_detected for scan_result in scan_results))
9999

100100

101-
def _should_use_scan_service(scan_type: str, scan_parameters: Optional[dict] = None) -> bool:
102-
return scan_type == consts.SECRET_SCAN_TYPE and scan_parameters is not None and scan_parameters['report'] is True
101+
def _should_use_scan_service(scan_type: str, scan_parameters: dict) -> bool:
102+
return scan_type == consts.SECRET_SCAN_TYPE and scan_parameters.get('report') is True
103+
104+
105+
def _should_use_sync_flow(scan_type: str, sync_option: bool, scan_parameters: Optional[dict] = None) -> bool:
106+
if not sync_option:
107+
return False
108+
109+
if scan_type not in (consts.SCA_SCAN_TYPE,):
110+
raise ValueError(f'Sync scan is not available for {scan_type} scan type.')
111+
112+
if scan_parameters.get('report') is True:
113+
raise ValueError('You can not use sync flow with report option. Either remove "report" or "sync" option.')
114+
115+
return True
103116

104117

105118
def _enrich_scan_result_with_data_from_detection_rules(
@@ -141,6 +154,7 @@ def _get_scan_documents_thread_func(
141154
cycode_client = context.obj['client']
142155
scan_type = context.obj['scan_type']
143156
severity_threshold = context.obj['severity_threshold']
157+
sync_option = context.obj['sync']
144158
command_scan_type = context.info_name
145159

146160
scan_parameters['aggregation_id'] = str(_generate_unique_id())
@@ -151,7 +165,9 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local
151165

152166
scan_id = str(_generate_unique_id())
153167
scan_completed = False
168+
154169
should_use_scan_service = _should_use_scan_service(scan_type, scan_parameters)
170+
should_use_sync_flow = _should_use_sync_flow(scan_type, sync_option, scan_parameters)
155171

156172
try:
157173
logger.debug('Preparing local files, %s', {'batch_size': len(batch)})
@@ -166,6 +182,7 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local
166182
is_commit_range,
167183
scan_parameters,
168184
should_use_scan_service,
185+
should_use_sync_flow,
169186
)
170187

171188
_enrich_scan_result_with_data_from_detection_rules(cycode_client, scan_type, scan_result)
@@ -439,7 +456,11 @@ def perform_scan(
439456
is_commit_range: bool,
440457
scan_parameters: dict,
441458
should_use_scan_service: bool = False,
459+
should_use_sync_flow: bool = False,
442460
) -> ZippedFileScanResult:
461+
if should_use_sync_flow:
462+
return perform_scan_sync(cycode_client, zipped_documents, scan_type, scan_parameters)
463+
443464
if scan_type in (consts.SCA_SCAN_TYPE, consts.SAST_SCAN_TYPE) or should_use_scan_service:
444465
return perform_scan_async(cycode_client, zipped_documents, scan_type, scan_parameters)
445466

@@ -466,6 +487,21 @@ def perform_scan_async(
466487
)
467488

468489

490+
def perform_scan_sync(
491+
cycode_client: 'ScanClient',
492+
zipped_documents: 'InMemoryZip',
493+
scan_type: str,
494+
scan_parameters: dict,
495+
) -> ZippedFileScanResult:
496+
scan_results = cycode_client.zipped_file_scan_sync(zipped_documents, scan_type, scan_parameters)
497+
logger.debug('scan request has been triggered successfully, scan id: %s', scan_results.id)
498+
return ZippedFileScanResult(
499+
did_detect=True,
500+
detections_per_file=_map_detections_per_file(scan_results.detection_messages),
501+
scan_id=scan_results.id,
502+
)
503+
504+
469505
def perform_commit_range_scan_async(
470506
cycode_client: 'ScanClient',
471507
from_commit_zipped_documents: 'InMemoryZip',
@@ -888,10 +924,10 @@ def _map_detections_per_file(detections: List[dict]) -> List[DetectionsPerFile]:
888924

889925

890926
def _get_file_name_from_detection(detection: dict) -> str:
891-
if detection['category'] == 'SAST':
927+
if detection.get('category') == 'SAST':
892928
return detection['detection_details']['file_path']
893929

894-
if detection['category'] == 'SecretDetection':
930+
if detection.get('category') == 'SecretDetection':
895931
return _get_secret_file_name_from_detection(detection)
896932

897933
return detection['detection_details']['file_name']

cycode/cli/commands/scan/scan_command.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
'--scan-type',
3535
'-t',
3636
default='secret',
37-
help='Specify the type of scan you wish to execute (the default is Secrets)',
37+
help='Specify the type of scan you wish to execute (the default is Secrets).',
3838
type=click.Choice(config['scans']['supported_scans']),
3939
)
4040
@click.option(
@@ -100,6 +100,14 @@
100100
type=bool,
101101
required=False,
102102
)
103+
@click.option(
104+
'--sync',
105+
is_flag=True,
106+
default=False,
107+
help='Run scan synchronously (the default is asynchronous).',
108+
type=bool,
109+
required=False,
110+
)
103111
@click.pass_context
104112
def scan_command(
105113
context: click.Context,
@@ -113,6 +121,7 @@ def scan_command(
113121
monitor: bool,
114122
report: bool,
115123
no_restore: bool,
124+
sync: bool,
116125
) -> int:
117126
"""Scans for Secrets, IaC, SCA or SAST violations."""
118127
if show_secret:
@@ -127,6 +136,7 @@ def scan_command(
127136

128137
context.obj['client'] = get_scan_cycode_client(client_id, secret, not context.obj['show_secret'])
129138
context.obj['scan_type'] = scan_type
139+
context.obj['sync'] = sync
130140
context.obj['severity_threshold'] = severity_threshold
131141
context.obj['monitor'] = monitor
132142
context.obj['report'] = report

cycode/cli/models.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from enum import Enum
33
from typing import Dict, List, NamedTuple, Optional, Type
44

5-
from cycode.cyclient import logger
65
from cycode.cyclient.models import Detection
76

87

@@ -46,8 +45,7 @@ def try_get_value(name: str) -> any:
4645
@staticmethod
4746
def get_member_weight(name: str) -> any:
4847
weight = Severity.try_get_value(name)
49-
if weight is None:
50-
logger.debug(f'missing severity in enum: {name}')
48+
if weight is None: # if License Compliance
5149
return -2
5250
return weight
5351

cycode/cyclient/cycode_client_base.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,14 @@ def _execute(
6262
url = self.build_full_url(self.api_url, endpoint)
6363
logger.debug(f'Executing {method.upper()} request to {url}')
6464

65+
timeout = self.timeout
66+
if 'timeout' in kwargs:
67+
timeout = kwargs['timeout']
68+
del kwargs['timeout']
69+
6570
try:
6671
headers = self.get_request_headers(headers, without_auth=without_auth)
67-
response = request(method=method, url=url, timeout=self.timeout, headers=headers, **kwargs)
72+
response = request(method=method, url=url, timeout=timeout, headers=headers, **kwargs)
6873

6974
content = 'HIDDEN' if hide_response_content_log else response.text
7075
logger.debug(f'Response {response.status_code} from {url}. Content: {content}')

cycode/cyclient/models.py

+18
Original file line numberDiff line numberDiff line change
@@ -453,3 +453,21 @@ class Meta:
453453
@post_load
454454
def build_dto(self, data: Dict[str, Any], **_) -> DetectionRule:
455455
return DetectionRule(**data)
456+
457+
458+
@dataclass
459+
class ScanResultsSyncFlow:
460+
id: str
461+
detection_messages: List[Dict]
462+
463+
464+
class ScanResultsSyncFlowSchema(Schema):
465+
class Meta:
466+
unknown = EXCLUDE
467+
468+
id = fields.String()
469+
detection_messages = fields.List(fields.Dict())
470+
471+
@post_load
472+
def build_dto(self, data: Dict[str, Any], **_) -> ScanResultsSyncFlow:
473+
return ScanResultsSyncFlow(**data)

cycode/cyclient/scan_client.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,20 @@ def get_detections_service_controller_path(self, scan_type: str) -> str:
4848

4949
return self._DETECTIONS_SERVICE_CLI_CONTROLLER_PATH
5050

51-
def get_scan_service_url_path(self, scan_type: str, should_use_scan_service: bool = False) -> str:
51+
@staticmethod
52+
def get_scan_flow_type(should_use_sync_flow: bool = False) -> str:
53+
if should_use_sync_flow:
54+
return '/sync'
55+
56+
return ''
57+
58+
def get_scan_service_url_path(
59+
self, scan_type: str, should_use_scan_service: bool = False, should_use_sync_flow: bool = False
60+
) -> str:
5261
service_path = self.scan_config.get_service_name(scan_type, should_use_scan_service)
5362
controller_path = self.get_scan_controller_path(scan_type)
54-
return f'{service_path}/{controller_path}'
63+
flow_type = self.get_scan_flow_type(should_use_sync_flow)
64+
return f'{service_path}/{controller_path}{flow_type}'
5565

5666
def content_scan(self, scan_type: str, file_name: str, content: str, is_git_diff: bool = True) -> models.ScanResult:
5767
path = f'{self.get_scan_service_url_path(scan_type)}/content'
@@ -82,12 +92,28 @@ def get_scan_report_url(self, scan_id: str, scan_type: str) -> models.ScanReport
8292
response = self.scan_cycode_client.get(url_path=self.get_scan_report_url_path(scan_id, scan_type))
8393
return models.ScanReportUrlResponseSchema().build_dto(response.json())
8494

85-
def get_zipped_file_scan_async_url_path(self, scan_type: str) -> str:
95+
def get_zipped_file_scan_async_url_path(self, scan_type: str, should_use_sync_flow: bool = False) -> str:
8696
async_scan_type = self.scan_config.get_async_scan_type(scan_type)
8797
async_entity_type = self.scan_config.get_async_entity_type(scan_type)
88-
scan_service_url_path = self.get_scan_service_url_path(scan_type, True)
98+
scan_service_url_path = self.get_scan_service_url_path(
99+
scan_type, should_use_scan_service=True, should_use_sync_flow=should_use_sync_flow
100+
)
89101
return f'{scan_service_url_path}/{async_scan_type}/{async_entity_type}'
90102

103+
def zipped_file_scan_sync(
104+
self, zip_file: InMemoryZip, scan_type: str, scan_parameters: dict
105+
) -> models.ScanResultsSyncFlow:
106+
files = {'file': ('multiple_files_scan.zip', zip_file.read())}
107+
del scan_parameters['report'] # BE raises validation error instead of ignoring it
108+
response = self.scan_cycode_client.post(
109+
url_path=self.get_zipped_file_scan_async_url_path(scan_type, should_use_sync_flow=True),
110+
data={'scan_parameters': json.dumps(scan_parameters)},
111+
files=files,
112+
hide_response_content_log=self._hide_response_log,
113+
timeout=60,
114+
)
115+
return models.ScanResultsSyncFlowSchema().load(response.json())
116+
91117
def zipped_file_scan_async(
92118
self,
93119
zip_file: InMemoryZip,

0 commit comments

Comments
 (0)