Skip to content

Commit a672f84

Browse files
authored
CM-30564 - Add support for report command in secret scanning (#203)
* CM-30564-Add support for report command in secret scanning * CM-30564-formatting * CM-30564-formatting * CM-30564-formatting * CM-30564-formatting * CM-30564-fix review * CM-30564-fix
1 parent ff8016c commit a672f84

File tree

9 files changed

+189
-38
lines changed

9 files changed

+189
-38
lines changed

cycode/cli/commands/scan/code_scanner.py

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import logging
32
import os
43
import sys
@@ -99,6 +98,10 @@ def set_issue_detected_by_scan_results(context: click.Context, scan_results: Lis
9998
set_issue_detected(context, any(scan_result.issue_detected for scan_result in scan_results))
10099

101100

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
103+
104+
102105
def _enrich_scan_result_with_data_from_detection_rules(
103106
cycode_client: 'ScanClient', scan_type: str, scan_result: ZippedFileScanResult
104107
) -> None:
@@ -148,14 +151,21 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local
148151

149152
scan_id = str(_generate_unique_id())
150153
scan_completed = False
154+
should_use_scan_service = _should_use_scan_service(scan_type, scan_parameters)
151155

152156
try:
153157
logger.debug('Preparing local files, %s', {'batch_size': len(batch)})
154158
zipped_documents = zip_documents(scan_type, batch)
155159
zip_file_size = zipped_documents.size
156-
157160
scan_result = perform_scan(
158-
cycode_client, zipped_documents, scan_type, scan_id, is_git_diff, is_commit_range, scan_parameters
161+
cycode_client,
162+
zipped_documents,
163+
scan_type,
164+
scan_id,
165+
is_git_diff,
166+
is_commit_range,
167+
scan_parameters,
168+
should_use_scan_service,
159169
)
160170

161171
_enrich_scan_result_with_data_from_detection_rules(cycode_client, scan_type, scan_result)
@@ -194,6 +204,7 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local
194204
zip_file_size,
195205
command_scan_type,
196206
error_message,
207+
should_use_scan_service,
197208
)
198209

199210
return scan_id, error, local_scan_result
@@ -315,14 +326,13 @@ def scan_commit_range_documents(
315326
local_scan_result = error_message = None
316327
scan_completed = False
317328
scan_id = str(_generate_unique_id())
318-
319329
from_commit_zipped_documents = InMemoryZip()
320330
to_commit_zipped_documents = InMemoryZip()
321331

322332
try:
323333
progress_bar.set_section_length(ScanProgressBarSection.SCAN, 1)
324334

325-
scan_result = init_default_scan_result(scan_id)
335+
scan_result = init_default_scan_result(cycode_client, scan_id, scan_type)
326336
if should_scan_documents(from_documents_to_scan, to_documents_to_scan):
327337
logger.debug('Preparing from-commit zip')
328338
from_commit_zipped_documents = zip_documents(scan_type, from_documents_to_scan)
@@ -428,8 +438,9 @@ def perform_scan(
428438
is_git_diff: bool,
429439
is_commit_range: bool,
430440
scan_parameters: dict,
441+
should_use_scan_service: bool = False,
431442
) -> ZippedFileScanResult:
432-
if scan_type in (consts.SCA_SCAN_TYPE, consts.SAST_SCAN_TYPE):
443+
if scan_type in (consts.SCA_SCAN_TYPE, consts.SAST_SCAN_TYPE) or should_use_scan_service:
433444
return perform_scan_async(cycode_client, zipped_documents, scan_type, scan_parameters)
434445

435446
if is_commit_range:
@@ -439,12 +450,20 @@ def perform_scan(
439450

440451

441452
def perform_scan_async(
442-
cycode_client: 'ScanClient', zipped_documents: 'InMemoryZip', scan_type: str, scan_parameters: dict
453+
cycode_client: 'ScanClient',
454+
zipped_documents: 'InMemoryZip',
455+
scan_type: str,
456+
scan_parameters: dict,
443457
) -> ZippedFileScanResult:
444458
scan_async_result = cycode_client.zipped_file_scan_async(zipped_documents, scan_type, scan_parameters)
445459
logger.debug('scan request has been triggered successfully, scan id: %s', scan_async_result.scan_id)
446460

447-
return poll_scan_results(cycode_client, scan_async_result.scan_id, scan_type)
461+
return poll_scan_results(
462+
cycode_client,
463+
scan_async_result.scan_id,
464+
scan_type,
465+
scan_parameters.get('report'),
466+
)
448467

449468

450469
def perform_commit_range_scan_async(
@@ -460,13 +479,16 @@ def perform_commit_range_scan_async(
460479
)
461480

462481
logger.debug('scan request has been triggered successfully, scan id: %s', scan_async_result.scan_id)
463-
return poll_scan_results(cycode_client, scan_async_result.scan_id, scan_type, timeout)
482+
return poll_scan_results(
483+
cycode_client, scan_async_result.scan_id, scan_type, scan_parameters.get('report'), timeout
484+
)
464485

465486

466487
def poll_scan_results(
467488
cycode_client: 'ScanClient',
468489
scan_id: str,
469490
scan_type: str,
491+
should_get_report: bool = False,
470492
polling_timeout: Optional[int] = None,
471493
) -> ZippedFileScanResult:
472494
if polling_timeout is None:
@@ -483,7 +505,7 @@ def poll_scan_results(
483505
print_debug_scan_details(scan_details)
484506

485507
if scan_details.scan_status == consts.SCAN_STATUS_COMPLETED:
486-
return _get_scan_result(cycode_client, scan_type, scan_id, scan_details)
508+
return _get_scan_result(cycode_client, scan_type, scan_id, scan_details, should_get_report)
487509

488510
if scan_details.scan_status == consts.SCAN_STATUS_ERROR:
489511
raise custom_exceptions.ScanAsyncError(
@@ -735,6 +757,7 @@ def _report_scan_status(
735757
zip_size: int,
736758
command_scan_type: str,
737759
error_message: Optional[str],
760+
should_use_scan_service: bool = False,
738761
) -> None:
739762
try:
740763
end_scan_time = time.time()
@@ -751,7 +774,7 @@ def _report_scan_status(
751774
'scan_type': scan_type,
752775
}
753776

754-
cycode_client.report_scan_status(scan_type, scan_id, scan_status)
777+
cycode_client.report_scan_status(scan_type, scan_id, scan_status, should_use_scan_service)
755778
except Exception as e:
756779
logger.debug('Failed to report scan status, %s', {'exception_message': str(e)})
757780

@@ -769,37 +792,49 @@ def _does_severity_match_severity_threshold(severity: str, severity_threshold: s
769792

770793

771794
def _get_scan_result(
772-
cycode_client: 'ScanClient', scan_type: str, scan_id: str, scan_details: 'ScanDetailsResponse'
795+
cycode_client: 'ScanClient',
796+
scan_type: str,
797+
scan_id: str,
798+
scan_details: 'ScanDetailsResponse',
799+
should_get_report: bool = False,
773800
) -> ZippedFileScanResult:
774801
if not scan_details.detections_count:
775-
return init_default_scan_result(scan_id, scan_details.metadata)
802+
return init_default_scan_result(cycode_client, scan_id, scan_type, should_get_report)
776803

777804
wait_for_detections_creation(cycode_client, scan_type, scan_id, scan_details.detections_count)
778805

779806
scan_detections = cycode_client.get_scan_detections(scan_type, scan_id)
807+
780808
return ZippedFileScanResult(
781809
did_detect=True,
782810
detections_per_file=_map_detections_per_file(scan_detections),
783811
scan_id=scan_id,
784-
report_url=_try_get_report_url(scan_details.metadata),
812+
report_url=_try_get_report_url_if_needed(cycode_client, should_get_report, scan_id, scan_type),
785813
)
786814

787815

788-
def init_default_scan_result(scan_id: str, scan_metadata: Optional[str] = None) -> ZippedFileScanResult:
816+
def init_default_scan_result(
817+
cycode_client: 'ScanClient', scan_id: str, scan_type: str, should_get_report: bool = False
818+
) -> ZippedFileScanResult:
789819
return ZippedFileScanResult(
790-
did_detect=False, detections_per_file=[], scan_id=scan_id, report_url=_try_get_report_url(scan_metadata)
820+
did_detect=False,
821+
detections_per_file=[],
822+
scan_id=scan_id,
823+
report_url=_try_get_report_url_if_needed(cycode_client, should_get_report, scan_id, scan_type),
791824
)
792825

793826

794-
def _try_get_report_url(metadata_json: Optional[str]) -> Optional[str]:
795-
if metadata_json is None:
827+
def _try_get_report_url_if_needed(
828+
cycode_client: 'ScanClient', should_get_report: bool, scan_id: str, scan_type: str
829+
) -> Optional[str]:
830+
if not should_get_report:
796831
return None
797832

798833
try:
799-
metadata_json = json.loads(metadata_json)
800-
return metadata_json.get('report_url')
801-
except json.JSONDecodeError:
802-
return None
834+
report_url_response = cycode_client.get_scan_report_url(scan_id, scan_type)
835+
return report_url_response.report_url
836+
except Exception as e:
837+
logger.debug('Failed to get report url: %s', str(e))
803838

804839

805840
def wait_for_detections_creation(
@@ -856,9 +891,18 @@ def _get_file_name_from_detection(detection: dict) -> str:
856891
if detection['category'] == 'SAST':
857892
return detection['detection_details']['file_path']
858893

894+
if detection['category'] == 'SecretDetection':
895+
return _get_secret_file_name_from_detection(detection)
896+
859897
return detection['detection_details']['file_name']
860898

861899

900+
def _get_secret_file_name_from_detection(detection: dict) -> str:
901+
file_path: str = detection['detection_details']['file_path']
902+
file_name: str = detection['detection_details']['file_name']
903+
return os.path.join(file_path, file_name)
904+
905+
862906
def _does_reach_to_max_commits_to_scan_limit(commit_ids: List[str], max_commits_count: Optional[int]) -> bool:
863907
if max_commits_count is None:
864908
return False

cycode/cyclient/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,19 @@ def __init__(
171171
self.err = err
172172

173173

174+
@dataclass
175+
class ScanReportUrlResponse:
176+
report_url: str
177+
178+
179+
class ScanReportUrlResponseSchema(Schema):
180+
report_url = fields.String()
181+
182+
@post_load
183+
def build_dto(self, data: Dict[str, Any], **_) -> 'ScanReportUrlResponse':
184+
return ScanReportUrlResponse(**data)
185+
186+
174187
class ScanDetailsResponseSchema(Schema):
175188
class Meta:
176189
unknown = EXCLUDE

cycode/cyclient/scan_client.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ def __init__(
3030

3131
self._hide_response_log = hide_response_log
3232

33-
def get_scan_controller_path(self, scan_type: str) -> str:
33+
def get_scan_controller_path(self, scan_type: str, should_use_scan_service: bool = False) -> str:
34+
if should_use_scan_service:
35+
return self._SCAN_CONTROLLER_PATH
3436
if scan_type == consts.SCA_SCAN_TYPE:
3537
return self._SCAN_CONTROLLER_PATH_SCA
3638

@@ -42,9 +44,9 @@ def get_detections_service_controller_path(self, scan_type: str) -> str:
4244

4345
return self._DETECTIONS_SERVICE_CONTROLLER_PATH
4446

45-
def get_scan_service_url_path(self, scan_type: str) -> str:
46-
service_path = self.scan_config.get_service_name(scan_type)
47-
controller_path = self.get_scan_controller_path(scan_type)
47+
def get_scan_service_url_path(self, scan_type: str, should_use_scan_service: bool = False) -> str:
48+
service_path = self.scan_config.get_service_name(scan_type, should_use_scan_service)
49+
controller_path = self.get_scan_controller_path(scan_type, should_use_scan_service)
4850
return f'{service_path}/{controller_path}'
4951

5052
def content_scan(self, scan_type: str, file_name: str, content: str, is_git_diff: bool = True) -> models.ScanResult:
@@ -72,13 +74,22 @@ def zipped_file_scan(
7274

7375
return self.parse_zipped_file_scan_response(response)
7476

77+
def get_scan_report_url(self, scan_id: str, scan_type: str) -> models.ScanReportUrlResponse:
78+
response = self.scan_cycode_client.get(url_path=self.get_scan_report_url_path(scan_id, scan_type))
79+
return models.ScanReportUrlResponseSchema().build_dto(response.json())
80+
7581
def get_zipped_file_scan_async_url_path(self, scan_type: str) -> str:
7682
async_scan_type = self.scan_config.get_async_scan_type(scan_type)
7783
async_entity_type = self.scan_config.get_async_entity_type(scan_type)
78-
return f'{self.get_scan_service_url_path(scan_type)}/{async_scan_type}/{async_entity_type}'
84+
scan_service_url_path = self.get_scan_service_url_path(scan_type, True)
85+
return f'{scan_service_url_path}/{async_scan_type}/{async_entity_type}'
7986

8087
def zipped_file_scan_async(
81-
self, zip_file: InMemoryZip, scan_type: str, scan_parameters: dict, is_git_diff: bool = False
88+
self,
89+
zip_file: InMemoryZip,
90+
scan_type: str,
91+
scan_parameters: dict,
92+
is_git_diff: bool = False,
8293
) -> models.ScanInitializationResponse:
8394
files = {'file': ('multiple_files_scan.zip', zip_file.read())}
8495
response = self.scan_cycode_client.post(
@@ -109,7 +120,10 @@ def multiple_zipped_file_scan_async(
109120
return models.ScanInitializationResponseSchema().load(response.json())
110121

111122
def get_scan_details_path(self, scan_type: str, scan_id: str) -> str:
112-
return f'{self.get_scan_service_url_path(scan_type)}/{scan_id}'
123+
return f'{self.get_scan_service_url_path(scan_type, should_use_scan_service=True)}/{scan_id}'
124+
125+
def get_scan_report_url_path(self, scan_id: str, scan_type: str) -> str:
126+
return f'{self.get_scan_service_url_path(scan_type, should_use_scan_service=True)}/reportUrl/{scan_id}'
113127

114128
def get_scan_details(self, scan_type: str, scan_id: str) -> models.ScanDetailsResponse:
115129
path = self.get_scan_details_path(scan_type, scan_id)
@@ -222,11 +236,18 @@ def commit_range_zipped_file_scan(
222236
)
223237
return self.parse_zipped_file_scan_response(response)
224238

225-
def get_report_scan_status_path(self, scan_type: str, scan_id: str) -> str:
226-
return f'{self.get_scan_service_url_path(scan_type)}/{scan_id}/status'
239+
def get_report_scan_status_path(self, scan_type: str, scan_id: str, should_use_scan_service: bool = False) -> str:
240+
return f'{self.get_scan_service_url_path(scan_type, should_use_scan_service)}/{scan_id}/status'
227241

228-
def report_scan_status(self, scan_type: str, scan_id: str, scan_status: dict) -> None:
229-
self.scan_cycode_client.post(url_path=self.get_report_scan_status_path(scan_type, scan_id), body=scan_status)
242+
def report_scan_status(
243+
self, scan_type: str, scan_id: str, scan_status: dict, should_use_scan_service: bool = False
244+
) -> None:
245+
self.scan_cycode_client.post(
246+
url_path=self.get_report_scan_status_path(
247+
scan_type, scan_id, should_use_scan_service=should_use_scan_service
248+
),
249+
body=scan_status,
250+
)
230251

231252
@staticmethod
232253
def parse_scan_response(response: Response) -> models.ScanResult:

cycode/cyclient/scan_config_base.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from abc import ABC, abstractmethod
22

3+
from cycode.cli import consts
4+
35

46
class ScanConfigBase(ABC):
57
@abstractmethod
6-
def get_service_name(self, scan_type: str) -> str:
8+
def get_service_name(self, scan_type: str, should_use_scan_service: bool = False) -> str:
79
...
810

911
@staticmethod
@@ -16,7 +18,9 @@ def get_async_scan_type(scan_type: str) -> str:
1618
return scan_type.upper()
1719

1820
@staticmethod
19-
def get_async_entity_type(_: str) -> str:
21+
def get_async_entity_type(scan_type: str) -> str:
22+
if scan_type == consts.SECRET_SCAN_TYPE:
23+
return 'ZippedFile'
2024
# we are migrating to "zippedfile" entity type. will be used later
2125
return 'repository'
2226

@@ -26,7 +30,9 @@ def get_detections_prefix(self) -> str:
2630

2731

2832
class DevScanConfig(ScanConfigBase):
29-
def get_service_name(self, scan_type: str) -> str:
33+
def get_service_name(self, scan_type: str, should_use_scan_service: bool = False) -> str:
34+
if should_use_scan_service:
35+
return '5004'
3036
if scan_type == 'secret':
3137
return '5025'
3238
if scan_type == 'iac':
@@ -40,7 +46,9 @@ def get_detections_prefix(self) -> str:
4046

4147

4248
class DefaultScanConfig(ScanConfigBase):
43-
def get_service_name(self, scan_type: str) -> str:
49+
def get_service_name(self, scan_type: str, should_use_scan_service: bool = False) -> str:
50+
if should_use_scan_service:
51+
return 'scans'
4452
if scan_type == 'secret':
4553
return 'secret'
4654
if scan_type == 'iac':

tests/cyclient/mocked_responses/scan_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ def get_scan_details_url(scan_id: Optional[UUID], scan_client: ScanClient) -> st
7979
return f'{api_url}/{service_url}'
8080

8181

82+
def get_scan_report_url(scan_id: Optional[UUID], scan_client: ScanClient, scan_type: str) -> str:
83+
api_url = scan_client.scan_cycode_client.api_url
84+
service_url = scan_client.get_scan_report_url_path(str(scan_id), scan_type)
85+
return f'{api_url}/{service_url}'
86+
87+
88+
def get_scan_report_url_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response:
89+
if not scan_id:
90+
scan_id = uuid4()
91+
json_response = {'report_url': f'https://app.domain/on-demand-scans/{scan_id}'}
92+
93+
return responses.Response(method=responses.GET, url=url, json=json_response, status=200)
94+
95+
8296
def get_scan_details_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response:
8397
if not scan_id:
8498
scan_id = uuid4()
@@ -182,3 +196,4 @@ def mock_scan_responses(
182196
)
183197
responses_module.add(get_detection_rules_response(get_detection_rules_url(scan_client)))
184198
responses_module.add(get_report_scan_status_response(get_report_scan_status_url(scan_type, scan_id, scan_client)))
199+
responses_module.add(get_scan_report_url_response(get_scan_report_url(scan_id, scan_client, scan_type)))

0 commit comments

Comments
 (0)