Skip to content
Merged
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
19 changes: 5 additions & 14 deletions cycode/cli/apps/scan/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,26 +121,17 @@ def _should_use_sync_flow(command_scan_type: str, scan_type: str, sync_option: b
Passing `--sync` option does not mean that sync flow will be used in all cases.

The logic:
- for IAC scan, sync flow is always used
- for SAST scan, sync flow is not supported
- for SCA and Secrets scan, sync flow is supported only for path/repository scan
- for IaC and SAST scans, the async flow is always used
- for SCA and Secrets scans, sync flow is used only with `--sync` on path/repository scans

"""
if not sync_option and scan_type != consts.IAC_SCAN_TYPE:
if scan_type in {consts.IAC_SCAN_TYPE, consts.SAST_SCAN_TYPE}:
return False

if command_scan_type not in {'path', 'repository', 'ai_guardrails'}:
if not sync_option:
return False

if scan_type == consts.IAC_SCAN_TYPE:
# sync in the only available flow for IAC scan; we do not use detector directly anymore
return True

if scan_type is consts.SAST_SCAN_TYPE: # noqa: SIM103
# SAST does not support sync flow
return False

return True
return command_scan_type in {'path', 'repository', 'ai_guardrails'}


def _get_scan_documents_thread_func(
Expand Down
19 changes: 13 additions & 6 deletions cycode/cyclient/scan_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
if TYPE_CHECKING:
from cycode.cyclient.scan_config_base import ScanConfigBase

_DETECTION_RULES_IDS_PER_REQUEST = 50


class ScanClient:
def __init__(
Expand Down Expand Up @@ -317,13 +319,18 @@ def parse_detection_rules_response(response: Response) -> list[models.DetectionR
return models.DetectionRuleSchema().load(response.json(), many=True)

def get_detection_rules(self, detection_rules_ids: set[str] | list[str]) -> list[models.DetectionRule]:
response = self.scan_cycode_client.get(
url_path=self.get_detection_rules_path(),
params={'ids': detection_rules_ids},
hide_response_content_log=self._hide_response_log,
)
# The ids go in the query string; too many of them exceed the URL limit (HTTP 414).
ids = list(detection_rules_ids)
detection_rules = []
for start in range(0, len(ids), _DETECTION_RULES_IDS_PER_REQUEST):
response = self.scan_cycode_client.get(
url_path=self.get_detection_rules_path(),
params={'ids': ids[start : start + _DETECTION_RULES_IDS_PER_REQUEST]},
hide_response_content_log=self._hide_response_log,
)
detection_rules.extend(self.parse_detection_rules_response(response))

return self.parse_detection_rules_response(response)
return detection_rules

def get_scan_detections_path(self) -> str:
return f'{self.scan_config.get_detections_prefix()}/{self._DETECTIONS_SERVICE_CLI_CONTROLLER_PATH}'
Expand Down
21 changes: 21 additions & 0 deletions tests/cli/commands/scan/test_code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
_get_scan_documents_thread_func,
_perform_scan,
_run_presigned_upload_scan,
_should_use_sync_flow,
scan_disk_files,
scan_documents,
)
Expand All @@ -19,6 +20,26 @@
from cycode.cli.models import Document


@pytest.mark.parametrize(
('command_scan_type', 'scan_type', 'sync_option', 'expected'),
[
# IaC and SAST always use the async flow, regardless of --sync.
('path', consts.IAC_SCAN_TYPE, False, False),
('path', consts.IAC_SCAN_TYPE, True, False),
('repository', consts.IAC_SCAN_TYPE, True, False),
('path', consts.SAST_SCAN_TYPE, False, False),
('path', consts.SAST_SCAN_TYPE, True, False),
# SCA/Secrets use the sync flow only with --sync on path/repository.
('path', consts.SCA_SCAN_TYPE, True, True),
('repository', consts.SECRET_SCAN_TYPE, True, True),
('path', consts.SCA_SCAN_TYPE, False, False),
('commit_history', consts.SCA_SCAN_TYPE, True, False),
],
)
def test_should_use_sync_flow(command_scan_type: str, scan_type: str, sync_option: bool, expected: bool) -> None:
assert _should_use_sync_flow(command_scan_type, scan_type, sync_option) is expected


def test_is_file_relevant_for_sca_scan() -> None:
path = os.path.join('some_package', 'node_modules', 'package.json')
assert _is_file_relevant_for_sca_scan(path) is False
Expand Down
25 changes: 25 additions & 0 deletions tests/cyclient/test_scan_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
import os
from urllib.parse import parse_qs, urlparse
from uuid import uuid4

import pytest
Expand All @@ -20,6 +22,7 @@
from cycode.cyclient.scan_client import ScanClient
from tests.conftest import ZIP_CONTENT_PATH
from tests.cyclient.mocked_responses.scan_client import (
get_detection_rules_url,
get_scan_aggregation_report_url,
get_scan_aggregation_report_url_response,
get_scan_details_response,
Expand Down Expand Up @@ -172,6 +175,28 @@ def test_get_scan_details(
assert scan_details_response.scan_status == 'Completed'


@responses.activate
def test_get_detection_rules_splits_ids_into_small_requests(
scan_client: ScanClient, api_token_response: responses.Response
) -> None:
requested_ids_per_call: list[list[str]] = []

def detection_rules_callback(request: requests.PreparedRequest) -> tuple[int, dict, str]:
ids = parse_qs(urlparse(request.url).query)['ids']
requested_ids_per_call.append(ids)
rules = [{'detection_rule_id': rule_id, 'classification_data': []} for rule_id in ids]
return 200, {}, json.dumps(rules)

responses.add(api_token_response) # mock token based client
responses.add_callback(responses.GET, get_detection_rules_url(scan_client), callback=detection_rules_callback)

detection_rule_ids = [str(uuid4()) for _ in range(120)]
detection_rules = scan_client.get_detection_rules(detection_rule_ids)

assert max(len(ids) for ids in requested_ids_per_call) <= 50
assert sorted(rule.detection_rule_id for rule in detection_rules) == sorted(detection_rule_ids)


@pytest.mark.parametrize('scan_type', list(ScanTypeOption))
def test_zipped_file_scan_async_slow_upload_error(
scan_type: ScanTypeOption, scan_client: ScanClient, mocker: MockerFixture
Expand Down
Loading