Skip to content

Commit 8b91279

Browse files
authored
CM-34882 - Add one report URL for all secrets found in the same scan (#228)
1 parent 1c46a55 commit 8b91279

File tree

10 files changed

+141
-32
lines changed

10 files changed

+141
-32
lines changed

cycode/cli/commands/scan/code_scanner.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,14 @@ def _enrich_scan_result_with_data_from_detection_rules(
148148

149149
def _get_scan_documents_thread_func(
150150
context: click.Context, is_git_diff: bool, is_commit_range: bool, scan_parameters: dict
151-
) -> Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]]:
151+
) -> Tuple[Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]], str]:
152152
cycode_client = context.obj['client']
153153
scan_type = context.obj['scan_type']
154154
severity_threshold = context.obj['severity_threshold']
155155
sync_option = context.obj['sync']
156156
command_scan_type = context.info_name
157-
158-
scan_parameters['aggregation_id'] = str(_generate_unique_id())
157+
aggregation_id = str(_generate_unique_id())
158+
scan_parameters['aggregation_id'] = aggregation_id
159159

160160
def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, LocalScanResult]:
161161
local_scan_result = error = error_message = None
@@ -224,7 +224,7 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local
224224

225225
return scan_id, error, local_scan_result
226226

227-
return _scan_batch_thread_func
227+
return _scan_batch_thread_func, aggregation_id
228228

229229

230230
def scan_commit_range(
@@ -313,11 +313,16 @@ def scan_documents(
313313
)
314314
return
315315

316-
scan_batch_thread_func = _get_scan_documents_thread_func(context, is_git_diff, is_commit_range, scan_parameters)
316+
scan_batch_thread_func, aggregation_id = _get_scan_documents_thread_func(
317+
context, is_git_diff, is_commit_range, scan_parameters
318+
)
317319
errors, local_scan_results = run_parallel_batched_scan(
318320
scan_batch_thread_func, documents_to_scan, progress_bar=progress_bar
319321
)
320-
322+
aggregation_report_url = _try_get_aggregation_report_url_if_needed(
323+
scan_parameters, context.obj['client'], context.obj['scan_type']
324+
)
325+
set_aggregation_report_url(context, aggregation_report_url)
321326
progress_bar.set_section_length(ScanProgressBarSection.GENERATE_REPORT, 1)
322327
progress_bar.update(ScanProgressBarSection.GENERATE_REPORT)
323328
progress_bar.stop()
@@ -326,6 +331,25 @@ def scan_documents(
326331
print_results(context, local_scan_results, errors)
327332

328333

334+
def set_aggregation_report_url(context: click.Context, aggregation_report_url: Optional[str] = None) -> None:
335+
context.obj['aggregation_report_url'] = aggregation_report_url
336+
337+
338+
def _try_get_aggregation_report_url_if_needed(
339+
scan_parameters: dict, cycode_client: 'ScanClient', scan_type: str
340+
) -> Optional[str]:
341+
aggregation_id = scan_parameters.get('aggregation_id')
342+
if not scan_parameters.get('report'):
343+
return None
344+
if aggregation_id is None:
345+
return None
346+
try:
347+
report_url_response = cycode_client.get_scan_aggregation_report_url(aggregation_id, scan_type)
348+
return report_url_response.report_url
349+
except Exception as e:
350+
logger.debug('Failed to get aggregation report url: %s', str(e))
351+
352+
329353
def scan_commit_range_documents(
330354
context: click.Context,
331355
from_documents_to_scan: List[Document],

cycode/cli/printers/console_printer.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ def __init__(self, context: click.Context) -> None:
2828
self.context = context
2929
self.scan_type = self.context.obj.get('scan_type')
3030
self.output_type = self.context.obj.get('output')
31-
31+
self.aggregation_report_url = self.context.obj.get('aggregation_report_url')
3232
self._printer_class = self._AVAILABLE_PRINTERS.get(self.output_type)
3333
if self._printer_class is None:
3434
raise CycodeError(f'"{self.output_type}" output type is not supported.')
3535

3636
def print_scan_results(
37-
self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None
37+
self,
38+
local_scan_results: List['LocalScanResult'],
39+
errors: Optional[Dict[str, 'CliError']] = None,
3840
) -> None:
3941
printer = self._get_scan_printer()
4042
printer.print_scan_results(local_scan_results, errors)

cycode/cli/printers/json_printer.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ def print_scan_results(
2828
scan_ids = []
2929
report_urls = []
3030
detections = []
31+
aggregation_report_url = self.context.obj.get('aggregation_report_url')
32+
if aggregation_report_url:
33+
report_urls.append(aggregation_report_url)
3134

3235
for local_scan_result in local_scan_results:
3336
scan_ids.append(local_scan_result.scan_id)
3437

35-
if local_scan_result.report_url:
38+
if not aggregation_report_url and local_scan_result.report_url:
3639
report_urls.append(local_scan_result.report_url)
37-
3840
for document_detections in local_scan_result.document_detections:
3941
detections.extend(document_detections.detections)
4042

cycode/cli/printers/tables/sca_table_printer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
if TYPE_CHECKING:
1414
from cycode.cli.models import LocalScanResult
1515

16-
1716
column_builder = ColumnInfoBuilder()
1817

1918
# Building must have strict order. Represents the order of the columns in the table (from left to right)
@@ -29,7 +28,6 @@
2928
DIRECT_DEPENDENCY_COLUMN = column_builder.build(name='Direct Dependency')
3029
DEVELOPMENT_DEPENDENCY_COLUMN = column_builder.build(name='Development Dependency')
3130

32-
3331
COLUMN_WIDTHS_CONFIG: ColumnWidths = {
3432
REPOSITORY_COLUMN: 2,
3533
CODE_PROJECT_COLUMN: 2,
@@ -42,6 +40,7 @@
4240

4341
class ScaTablePrinter(TablePrinterBase):
4442
def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
43+
aggregation_report_url = self.context.obj.get('aggregation_report_url')
4544
detections_per_policy_id = self._extract_detections_per_policy_id(local_scan_results)
4645
for policy_id, detections in detections_per_policy_id.items():
4746
table = self._get_table(policy_id)
@@ -53,7 +52,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
5352
self._print_summary_issues(len(detections), self._get_title(policy_id))
5453
self._print_table(table)
5554

56-
self._print_report_urls(local_scan_results)
55+
self._print_report_urls(local_scan_results, aggregation_report_url)
5756

5857
@staticmethod
5958
def _get_title(policy_id: str) -> str:

cycode/cli/printers/tables/table_printer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def _print_results(self, local_scan_results: List['LocalScanResult']) -> None:
6363
self._enrich_table_with_values(table, detection, document_detections.document)
6464

6565
self._print_table(table)
66-
self._print_report_urls(local_scan_results)
66+
self._print_report_urls(local_scan_results, self.context.obj.get('aggregation_report_url'))
6767

6868
def _get_table(self) -> Table:
6969
table = Table()

cycode/cli/printers/tables/table_printer_base.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,15 @@ def _print_table(table: 'Table') -> None:
5858
click.echo(table.get_table().draw())
5959

6060
@staticmethod
61-
def _print_report_urls(local_scan_results: List['LocalScanResult']) -> None:
61+
def _print_report_urls(
62+
local_scan_results: List['LocalScanResult'],
63+
aggregation_report_url: Optional[str] = None,
64+
) -> None:
6265
report_urls = [scan_result.report_url for scan_result in local_scan_results if scan_result.report_url]
63-
if not report_urls:
66+
if not report_urls and not aggregation_report_url:
67+
return
68+
if aggregation_report_url:
69+
click.echo(f'Report URL: {aggregation_report_url}')
6470
return
6571

6672
click.echo('Report URLs:')

cycode/cli/printers/text_printer.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ def print_scan_results(
3939

4040
for local_scan_result in local_scan_results:
4141
for document_detections in local_scan_result.document_detections:
42-
self._print_document_detections(
43-
document_detections, local_scan_result.scan_id, local_scan_result.report_url
44-
)
42+
self._print_document_detections(document_detections, local_scan_result.scan_id)
4543

44+
report_urls = [scan_result.report_url for scan_result in local_scan_results if scan_result.report_url]
45+
46+
self._print_report_urls(report_urls, self.context.obj.get('aggregation_report_url'))
4647
if not errors:
4748
return
4849

@@ -55,27 +56,21 @@ def print_scan_results(
5556
click.echo(f'- {scan_id}: ', nl=False)
5657
self.print_error(error)
5758

58-
def _print_document_detections(
59-
self, document_detections: DocumentDetections, scan_id: str, report_url: Optional[str]
60-
) -> None:
59+
def _print_document_detections(self, document_detections: DocumentDetections, scan_id: str) -> None:
6160
document = document_detections.document
6261
lines_to_display = self._get_lines_to_display_count()
6362
for detection in document_detections.detections:
64-
self._print_detection_summary(detection, document.path, scan_id, report_url)
63+
self._print_detection_summary(detection, document.path, scan_id)
6564
self._print_detection_code_segment(detection, document, lines_to_display)
6665

67-
def _print_detection_summary(
68-
self, detection: Detection, document_path: str, scan_id: str, report_url: Optional[str]
69-
) -> None:
66+
def _print_detection_summary(self, detection: Detection, document_path: str, scan_id: str) -> None:
7067
detection_name = detection.type if self.scan_type == SECRET_SCAN_TYPE else detection.message
7168
detection_name_styled = click.style(detection_name, fg='bright_red', bold=True)
7269

7370
detection_sha = detection.detection_details.get('sha512')
7471
detection_sha_message = f'\nSecret SHA: {detection_sha}' if detection_sha else ''
7572

7673
scan_id_message = f'\nScan ID: {scan_id}'
77-
report_url_message = f'\nReport URL: {report_url}' if report_url else ''
78-
7974
detection_commit_id = detection.detection_details.get('commit_id')
8075
detection_commit_id_message = f'\nCommit SHA: {detection_commit_id}' if detection_commit_id else ''
8176

@@ -88,7 +83,6 @@ def _print_detection_summary(
8883
f'(rule ID: {detection.detection_rule_id}) in file: {click.format_filename(document_path)} '
8984
f'{detection_sha_message}'
9085
f'{scan_id_message}'
91-
f'{report_url_message}'
9286
f'{detection_commit_id_message}'
9387
f'{company_guidelines_message}'
9488
f' ⛔'
@@ -101,6 +95,18 @@ def _print_detection_code_segment(self, detection: Detection, document: Document
10195

10296
self._print_detection_from_file(detection, document, code_segment_size)
10397

98+
@staticmethod
99+
def _print_report_urls(report_urls: List[str], aggregation_report_url: Optional[str] = None) -> None:
100+
if not report_urls and not aggregation_report_url:
101+
return
102+
if aggregation_report_url:
103+
click.echo(f'Report URL: {aggregation_report_url}')
104+
return
105+
106+
click.echo('Report URLs:')
107+
for report_url in report_urls:
108+
click.echo(f'- {report_url}')
109+
104110
@staticmethod
105111
def _get_code_segment_start_line(detection_line: int, code_segment_size: int) -> int:
106112
start_line = detection_line - math.ceil(code_segment_size / 2)

cycode/cyclient/scan_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def get_scan_service_url_path(
5959
self, scan_type: str, should_use_scan_service: bool = False, should_use_sync_flow: bool = False
6060
) -> str:
6161
service_path = self.scan_config.get_service_name(scan_type, should_use_scan_service)
62-
controller_path = self.get_scan_controller_path(scan_type)
62+
controller_path = self.get_scan_controller_path(scan_type, should_use_scan_service)
6363
flow_type = self.get_scan_flow_type(should_use_sync_flow)
6464
return f'{service_path}/{controller_path}{flow_type}'
6565

@@ -92,6 +92,12 @@ def get_scan_report_url(self, scan_id: str, scan_type: str) -> models.ScanReport
9292
response = self.scan_cycode_client.get(url_path=self.get_scan_report_url_path(scan_id, scan_type))
9393
return models.ScanReportUrlResponseSchema().build_dto(response.json())
9494

95+
def get_scan_aggregation_report_url(self, aggregation_id: str, scan_type: str) -> models.ScanReportUrlResponse:
96+
response = self.scan_cycode_client.get(
97+
url_path=self.get_scan_aggregation_report_url_path(aggregation_id, scan_type)
98+
)
99+
return models.ScanReportUrlResponseSchema().build_dto(response.json())
100+
95101
def get_zipped_file_scan_async_url_path(self, scan_type: str, should_use_sync_flow: bool = False) -> str:
96102
async_scan_type = self.scan_config.get_async_scan_type(scan_type)
97103
async_entity_type = self.scan_config.get_async_entity_type(scan_type)
@@ -155,6 +161,12 @@ def get_scan_details_path(self, scan_type: str, scan_id: str) -> str:
155161
def get_scan_report_url_path(self, scan_id: str, scan_type: str) -> str:
156162
return f'{self.get_scan_service_url_path(scan_type, should_use_scan_service=True)}/reportUrl/{scan_id}'
157163

164+
def get_scan_aggregation_report_url_path(self, aggregation_id: str, scan_type: str) -> str:
165+
return (
166+
f'{self.get_scan_service_url_path(scan_type, should_use_scan_service=True)}'
167+
f'/reportUrlByAggregationId/{aggregation_id}'
168+
)
169+
158170
def get_scan_details(self, scan_type: str, scan_id: str) -> models.ScanDetailsResponse:
159171
path = self.get_scan_details_path(scan_type, scan_id)
160172
response = self.scan_cycode_client.get(url_path=path)

tests/cyclient/mocked_responses/scan_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ def get_scan_report_url(scan_id: Optional[UUID], scan_client: ScanClient, scan_t
8585
return f'{api_url}/{service_url}'
8686

8787

88+
def get_scan_aggregation_report_url(aggregation_id: Optional[UUID], scan_client: ScanClient, scan_type: str) -> str:
89+
api_url = scan_client.scan_cycode_client.api_url
90+
service_url = scan_client.get_scan_aggregation_report_url_path(str(aggregation_id), scan_type)
91+
return f'{api_url}/{service_url}'
92+
93+
8894
def get_scan_report_url_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response:
8995
if not scan_id:
9096
scan_id = uuid4()
@@ -93,6 +99,14 @@ def get_scan_report_url_response(url: str, scan_id: Optional[UUID] = None) -> re
9399
return responses.Response(method=responses.GET, url=url, json=json_response, status=200)
94100

95101

102+
def get_scan_aggregation_report_url_response(url: str, aggregation_id: Optional[UUID] = None) -> responses.Response:
103+
if not aggregation_id:
104+
aggregation_id = uuid4()
105+
json_response = {'report_url': f'https://app.domain/cli-logs-aggregation/{aggregation_id}'}
106+
107+
return responses.Response(method=responses.GET, url=url, json=json_response, status=200)
108+
109+
96110
def get_scan_details_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response:
97111
if not scan_id:
98112
scan_id = uuid4()

tests/test_code_scanner.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44
import pytest
55
import responses
66

7-
from cycode.cli.commands.scan.code_scanner import _try_get_report_url_if_needed
7+
from cycode.cli.commands.scan.code_scanner import (
8+
_try_get_aggregation_report_url_if_needed,
9+
_try_get_report_url_if_needed,
10+
)
811
from cycode.cli.config import config
912
from cycode.cli.files_collector.excluder import _is_relevant_file_to_scan
1013
from cycode.cyclient.scan_client import ScanClient
1114
from tests.conftest import TEST_FILES_PATH
12-
from tests.cyclient.mocked_responses.scan_client import get_scan_report_url, get_scan_report_url_response
15+
from tests.cyclient.mocked_responses.scan_client import (
16+
get_scan_aggregation_report_url,
17+
get_scan_aggregation_report_url_response,
18+
get_scan_report_url,
19+
get_scan_report_url_response,
20+
)
1321

1422

1523
def test_is_relevant_file_to_scan_sca() -> None:
@@ -37,3 +45,39 @@ def test_try_get_report_url_if_needed_return_result(
3745
scan_report_url_response = scan_client.get_scan_report_url(str(scan_id), scan_type)
3846
result = _try_get_report_url_if_needed(scan_client, True, str(scan_id), scan_type)
3947
assert result == scan_report_url_response.report_url
48+
49+
50+
@pytest.mark.parametrize('scan_type', config['scans']['supported_scans'])
51+
def test_try_get_aggregation_report_url_if_no_report_command_needed_return_none(
52+
scan_type: str, scan_client: ScanClient
53+
) -> None:
54+
aggregation_id = uuid4().hex
55+
scan_parameter = {'aggregation_id': aggregation_id}
56+
result = _try_get_aggregation_report_url_if_needed(scan_parameter, scan_client, scan_type)
57+
assert result is None
58+
59+
60+
@pytest.mark.parametrize('scan_type', config['scans']['supported_scans'])
61+
def test_try_get_aggregation_report_url_if_no_aggregation_id_needed_return_none(
62+
scan_type: str, scan_client: ScanClient
63+
) -> None:
64+
scan_parameter = {'report': True}
65+
result = _try_get_aggregation_report_url_if_needed(scan_parameter, scan_client, scan_type)
66+
assert result is None
67+
68+
69+
@pytest.mark.parametrize('scan_type', config['scans']['supported_scans'])
70+
@responses.activate
71+
def test_try_get_aggregation_report_url_if_needed_return_result(
72+
scan_type: str, scan_client: ScanClient, api_token_response: responses.Response
73+
) -> None:
74+
aggregation_id = uuid4()
75+
scan_parameter = {'report': True, 'aggregation_id': aggregation_id}
76+
url = get_scan_aggregation_report_url(aggregation_id, scan_client, scan_type)
77+
responses.add(api_token_response) # mock token based client
78+
responses.add(get_scan_aggregation_report_url_response(url, aggregation_id))
79+
80+
scan_aggregation_report_url_response = scan_client.get_scan_aggregation_report_url(str(aggregation_id), scan_type)
81+
82+
result = _try_get_aggregation_report_url_if_needed(scan_parameter, scan_client, scan_type)
83+
assert result == scan_aggregation_report_url_response.report_url

0 commit comments

Comments
 (0)