From 8092428b425a6a65f6614b03ff3af526ab87b3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fabianski?= Date: Fri, 16 Oct 2026 16:41:25 +0200 Subject: [PATCH 1/2] CM-75484: Use the async scan flow for IaC scans IaC scans now use the async scan flow, like SAST, instead of the sync flow. Co-Authored-By: Claude Opus 4.8 --- cycode/cli/apps/scan/code_scanner.py | 19 +++++------------- tests/cli/commands/scan/test_code_scanner.py | 21 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index 8c321c56..6244103b 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -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( diff --git a/tests/cli/commands/scan/test_code_scanner.py b/tests/cli/commands/scan/test_code_scanner.py index 24f6a39f..3aa6cfae 100644 --- a/tests/cli/commands/scan/test_code_scanner.py +++ b/tests/cli/commands/scan/test_code_scanner.py @@ -10,6 +10,7 @@ _get_scan_documents_thread_func, _perform_scan, _run_presigned_upload_scan, + _should_use_sync_flow, scan_disk_files, scan_documents, ) @@ -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 From c0791f418c4421dc48c09aaa28a23f4192e7f846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fabianski?= Date: Thu, 5 Nov 2026 11:42:35 +0100 Subject: [PATCH 2/2] CM-75484: Request detection rules in chunks of 50 ids A large scan batch has hundreds of distinct rule ids, and sending them all in one query string fails with HTTP 414, which drops the batch. Co-Authored-By: Claude Opus 5.5 --- cycode/cyclient/scan_client.py | 19 +++++++++++++------ tests/cyclient/test_scan_client.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/cycode/cyclient/scan_client.py b/cycode/cyclient/scan_client.py index ea9a3734..4724e27a 100644 --- a/cycode/cyclient/scan_client.py +++ b/cycode/cyclient/scan_client.py @@ -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__( @@ -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}' diff --git a/tests/cyclient/test_scan_client.py b/tests/cyclient/test_scan_client.py index 505d8d50..937a86eb 100644 --- a/tests/cyclient/test_scan_client.py +++ b/tests/cyclient/test_scan_client.py @@ -1,4 +1,6 @@ +import json import os +from urllib.parse import parse_qs, urlparse from uuid import uuid4 import pytest @@ -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, @@ -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