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
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ This guide walks you through both installation and usage.
1. [Options](#options)
1. [Severity Threshold](#severity-option)
2. [Monitor](#monitor-option)
3. [Package Vulnerabilities](#package-vulnerabilities-option)
4. [License Compliance](#license-compliance-option)
5. [Lock Restore](#lock-restore-option)
3. [Cycode Report](#cycode-report-option)
4. [Package Vulnerabilities](#package-vulnerabilities-option)
5. [License Compliance](#license-compliance-option)
6. [Lock Restore](#lock-restore-option)
2. [Repository Scan](#repository-scan)
1. [Branch Option](#branch-option)
3. [Path Scan](#path-scan)
Expand Down Expand Up @@ -300,6 +301,7 @@ The Cycode CLI application offers several types of scans so that you can choose
| `--severity-threshold [INFO\|LOW\|MEDIUM\|HIGH\|CRITICAL]` | Show only violations at the specified level or higher. |
| `--sca-scan` | Specify the SCA scan you wish to execute (`package-vulnerabilities`/`license-compliance`). The default is both. |
| `--monitor` | When specified, the scan results will be recorded in the knowledge graph. Please note that when working in `monitor` mode, the knowledge graph will not be updated as a result of SCM events (Push, Repo creation). (Supported for SCA scan type only). |
| `--cycode-report` | When specified, displays a link to the scan report in the Cycode platform in the console output. |
| `--no-restore` | When specified, Cycode will not run restore command. Will scan direct dependencies ONLY! |
| `--gradle-all-sub-projects` | When specified, Cycode will run gradle restore command for all sub projects. Should run from root project directory ONLY! |
| `--help` | Show options for given command. |
Expand Down Expand Up @@ -337,6 +339,25 @@ When using this option, the scan results from this scan will appear in the knowl
> [!WARNING]
> You must be an `owner` or an `admin` in Cycode to view the knowledge graph page.

#### Cycode Report Option

For every scan performed using the Cycode CLI, a report is automatically generated and its results are sent to Cycode. These results are tied to the relevant policies (e.g., [SCA policies](https://docs.cycode.com/docs/sca-policies) for Repository scans) within the Cycode platform.

To have the direct URL to this Cycode report printed in your CLI output after the scan completes, add the argument `--cycode-report` to your scan command.

`cycode scan --cycode-report repository ~/home/git/codebase`

All scan results from the CLI will appear in the CLI Logs section of Cycode. If you included the `--cycode-report` flag in your command, a direct link to the specific report will be displayed in your terminal following the scan results.

> [!WARNING]
> You must be an `owner` or an `admin` in Cycode to view this page.

![cli-report](https://raw.githubusercontent.com/cycodehq/cycode-cli/main/images/sca_report_url.png)

The report page will look something like below:

![](https://raw.githubusercontent.com/cycodehq/cycode-cli/main/images/scan_details.png)

#### Package Vulnerabilities Option

> [!NOTE]
Expand Down
10 changes: 7 additions & 3 deletions cycode/cli/apps/scan/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ def scan_documents(
scan_batch_thread_func, scan_type, documents_to_scan, progress_bar=progress_bar
)

aggregation_report_url = _try_get_aggregation_report_url(scan_parameters, ctx.obj['client'], scan_type)
aggregation_report_url = _try_get_aggregation_report_url_if_needed(scan_parameters, ctx.obj['client'], scan_type)
_set_aggregation_report_url(ctx, aggregation_report_url)

progress_bar.set_section_length(ScanProgressBarSection.GENERATE_REPORT, 1)
Expand Down Expand Up @@ -641,6 +641,7 @@ def parse_pre_receive_input() -> str:
def _get_default_scan_parameters(ctx: typer.Context) -> dict:
return {
'monitor': ctx.obj.get('monitor'),
'report': ctx.obj.get('report'),
'package_vulnerabilities': ctx.obj.get('package-vulnerabilities'),
'license_compliance': ctx.obj.get('license-compliance'),
'command_type': ctx.info_name.replace('-', '_'), # save backward compatibility
Expand Down Expand Up @@ -956,7 +957,7 @@ def _get_scan_result(
did_detect=True,
detections_per_file=_map_detections_per_file_and_commit_id(scan_type, scan_raw_detections),
scan_id=scan_id,
report_url=_try_get_aggregation_report_url(scan_parameters, cycode_client, scan_type),
report_url=_try_get_aggregation_report_url_if_needed(scan_parameters, cycode_client, scan_type),
)


Expand All @@ -972,9 +973,12 @@ def _set_aggregation_report_url(ctx: typer.Context, aggregation_report_url: Opti
ctx.obj['aggregation_report_url'] = aggregation_report_url


def _try_get_aggregation_report_url(
def _try_get_aggregation_report_url_if_needed(
scan_parameters: dict, cycode_client: 'ScanClient', scan_type: str
) -> Optional[str]:
if not scan_parameters.get('report', False):
return None

aggregation_id = scan_parameters.get('aggregation_id')
if aggregation_id is None:
return None
Expand Down
8 changes: 8 additions & 0 deletions cycode/cli/apps/scan/scan_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ def scan_command(
'--sync', help='Run scan synchronously (INTERNAL FOR IDEs).', show_default='asynchronously', hidden=True
),
] = False,
report: Annotated[
bool,
typer.Option(
'--cycode-report',
help='When specified, displays a link to the scan report in the Cycode platform in the console output.',
),
] = False,
show_secret: Annotated[
bool, typer.Option('--show-secret', help='Show Secrets in plain text.', rich_help_panel=_SECRET_RICH_HELP_PANEL)
] = False,
Expand Down Expand Up @@ -136,6 +143,7 @@ def scan_command(
ctx.obj['sync'] = sync
ctx.obj['severity_threshold'] = severity_threshold
ctx.obj['monitor'] = monitor
ctx.obj['report'] = report

if export_type and export_file:
console_printer = ctx.obj['console_printer']
Expand Down
6 changes: 6 additions & 0 deletions cycode/cyclient/scan_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from copy import deepcopy
from typing import TYPE_CHECKING, Union
from uuid import UUID

Expand Down Expand Up @@ -73,6 +74,11 @@ def zipped_file_scan_sync(
is_git_diff: bool = False,
) -> models.ScanResultsSyncFlow:
files = {'file': ('multiple_files_scan.zip', zip_file.read())}

scan_parameters = deepcopy(scan_parameters) # avoid mutating the original dict
if 'report' in scan_parameters:
del scan_parameters['report'] # BE raises validation error instead of ignoring it

response = self.scan_cycode_client.post(
url_path=self.get_zipped_file_scan_sync_url_path(scan_type),
data={
Expand Down
11 changes: 6 additions & 5 deletions tests/test_code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from cycode.cli import consts
from cycode.cli.apps.scan.code_scanner import (
_try_get_aggregation_report_url,
_try_get_aggregation_report_url_if_needed,
)
from cycode.cli.cli_types import ScanTypeOption
from cycode.cli.files_collector.excluder import _is_relevant_file_to_scan
Expand All @@ -29,15 +29,16 @@ def test_try_get_aggregation_report_url_if_no_report_command_needed_return_none(
) -> None:
aggregation_id = uuid4().hex
scan_parameter = {'aggregation_id': aggregation_id}
result = _try_get_aggregation_report_url(scan_parameter, scan_client, scan_type)
result = _try_get_aggregation_report_url_if_needed(scan_parameter, scan_client, scan_type)
assert result is None


@pytest.mark.parametrize('scan_type', list(ScanTypeOption))
def test_try_get_aggregation_report_url_if_no_aggregation_id_needed_return_none(
scan_type: ScanTypeOption, scan_client: ScanClient
) -> None:
result = _try_get_aggregation_report_url({}, scan_client, scan_type)
scan_parameter = {'report': True}
result = _try_get_aggregation_report_url_if_needed(scan_parameter, scan_client, scan_type)
assert result is None


Expand All @@ -47,12 +48,12 @@ def test_try_get_aggregation_report_url_if_needed_return_result(
scan_type: ScanTypeOption, scan_client: ScanClient, api_token_response: responses.Response
) -> None:
aggregation_id = uuid4()
scan_parameter = {'aggregation_id': aggregation_id}
scan_parameter = {'report': True, 'aggregation_id': aggregation_id}
url = get_scan_aggregation_report_url(aggregation_id, scan_client, scan_type)
responses.add(api_token_response) # mock token based client
responses.add(get_scan_aggregation_report_url_response(url, aggregation_id))

scan_aggregation_report_url_response = scan_client.get_scan_aggregation_report_url(str(aggregation_id), scan_type)

result = _try_get_aggregation_report_url(scan_parameter, scan_client, scan_type)
result = _try_get_aggregation_report_url_if_needed(scan_parameter, scan_client, scan_type)
assert result == scan_aggregation_report_url_response.report_url
Loading