Skip to content

Commit b0e5f27

Browse files
Merge branch 'main' into CM-60184-Scans-using-presigned-post-url
2 parents b8cdb6f + 718521a commit b0e5f27

26 files changed

+1081
-499
lines changed

README.md

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -668,15 +668,33 @@ In the previous example, if you wanted to only scan a branch named `dev`, you co
668668
> [!NOTE]
669669
> This option is only available to SCA scans.
670670

671-
We use the sbt-dependency-lock plugin to restore the lock file for SBT projects.
672-
To disable lock restore in use `--no-restore` option.
673-
674-
Prerequisites:
675-
* `sbt-dependency-lock` plugin: Install the plugin by adding the following line to `project/plugins.sbt`:
676-
677-
```text
678-
addSbtPlugin("software.purpledragon" % "sbt-dependency-lock" % "1.5.1")
679-
```
671+
When running an SCA scan, Cycode CLI automatically attempts to restore (generate) a dependency lockfile for each supported manifest file it finds. This allows scanning transitive dependencies, not just the ones listed directly in the manifest. To skip this step and scan only direct dependencies, use the `--no-restore` flag.
672+
673+
The following ecosystems support automatic lockfile restoration:
674+
675+
| Ecosystem | Manifest file | Lockfile generated | Tool invoked (when lockfile is absent) |
676+
|---|---|---|---|
677+
| npm | `package.json` | `package-lock.json` | `npm install --package-lock-only --ignore-scripts --no-audit` |
678+
| Yarn | `package.json` | `yarn.lock` | `yarn install --ignore-scripts` |
679+
| pnpm | `package.json` | `pnpm-lock.yaml` | `pnpm install --ignore-scripts` |
680+
| Deno | `deno.json` / `deno.jsonc` | `deno.lock` | *(read existing lockfile only)* |
681+
| Go | `go.mod` | `go.mod.graph` | `go list -m -json all` + `go mod graph` |
682+
| Maven | `pom.xml` | `bcde.mvndeps` | `mvn dependency:tree` |
683+
| Gradle | `build.gradle` / `build.gradle.kts` | `gradle-dependencies-generated.txt` | `gradle dependencies -q --console plain` |
684+
| SBT | `build.sbt` | `build.sbt.lock` | `sbt dependencyLockWrite` |
685+
| NuGet | `*.csproj` | `packages.lock.json` | `dotnet restore --use-lock-file` |
686+
| Ruby | `Gemfile` | `Gemfile.lock` | `bundle --quiet` |
687+
| Poetry | `pyproject.toml` | `poetry.lock` | `poetry lock` |
688+
| Pipenv | `Pipfile` | `Pipfile.lock` | `pipenv lock` |
689+
| PHP Composer | `composer.json` | `composer.lock` | `composer update --no-cache --no-install --no-scripts --ignore-platform-reqs` |
690+
691+
If a lockfile already exists alongside the manifest, Cycode reads it directly without running any install command.
692+
693+
**SBT prerequisite:** The `sbt-dependency-lock` plugin must be installed. Add the following line to `project/plugins.sbt`:
694+
695+
```text
696+
addSbtPlugin("software.purpledragon" % "sbt-dependency-lock" % "1.5.1")
697+
```
680698

681699
### Repository Scan
682700

@@ -1309,9 +1327,11 @@ For example:\
13091327
13101328
The `path` subcommand supports the following additional options:
13111329
1312-
| Option | Description |
1313-
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------|
1314-
| `--maven-settings-file` | For Maven only, allows using a custom [settings.xml](https://maven.apache.org/settings.html) file when building the dependency tree |
1330+
| Option | Description |
1331+
|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
1332+
| `--no-restore` | Skip lockfile restoration and scan direct dependencies only. See [Lock Restore Option](#lock-restore-option) for details. |
1333+
| `--gradle-all-sub-projects` | Run the Gradle restore command for all sub-projects (use from the root of a multi-project Gradle build). |
1334+
| `--maven-settings-file` | For Maven only, allows using a custom [settings.xml](https://maven.apache.org/settings.html) file when building the dependency tree. |
13151335
13161336
# Import Command
13171337

cycode/cli/apps/report/sbom/path/path_command.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import time
22
from pathlib import Path
3-
from typing import Annotated, Optional
3+
from typing import Annotated
44

55
import typer
66

77
from cycode.cli import consts
88
from cycode.cli.apps.report.sbom.common import create_sbom_report, send_report_feedback
9+
from cycode.cli.apps.sca_options import (
10+
GradleAllSubProjectsOption,
11+
MavenSettingsFileOption,
12+
NoRestoreOption,
13+
apply_sca_restore_options_to_context,
14+
)
915
from cycode.cli.exceptions.handle_report_sbom_errors import handle_report_exception
1016
from cycode.cli.files_collector.path_documents import get_relevant_documents
1117
from cycode.cli.files_collector.sca.sca_file_collector import add_sca_dependencies_tree_documents_if_needed
@@ -14,27 +20,18 @@
1420
from cycode.cli.utils.progress_bar import SbomReportProgressBarSection
1521
from cycode.cli.utils.scan_utils import is_cycodeignore_allowed_by_scan_config
1622

17-
_SCA_RICH_HELP_PANEL = 'SCA options'
18-
1923

2024
def path_command(
2125
ctx: typer.Context,
2226
path: Annotated[
2327
Path,
2428
typer.Argument(exists=True, resolve_path=True, help='Path to generate SBOM report for.', show_default=False),
2529
],
26-
maven_settings_file: Annotated[
27-
Optional[Path],
28-
typer.Option(
29-
'--maven-settings-file',
30-
show_default=False,
31-
help='When specified, Cycode will use this settings.xml file when building the maven dependency tree.',
32-
dir_okay=False,
33-
rich_help_panel=_SCA_RICH_HELP_PANEL,
34-
),
35-
] = None,
30+
no_restore: NoRestoreOption = False,
31+
gradle_all_sub_projects: GradleAllSubProjectsOption = False,
32+
maven_settings_file: MavenSettingsFileOption = None,
3633
) -> None:
37-
ctx.obj['maven_settings_file'] = maven_settings_file
34+
apply_sca_restore_options_to_context(ctx, no_restore, gradle_all_sub_projects, maven_settings_file)
3835

3936
client = get_report_cycode_client(ctx)
4037
report_parameters = ctx.obj['report_parameters']

cycode/cli/apps/sca_options.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from pathlib import Path
2+
from typing import Annotated, Optional
3+
4+
import typer
5+
6+
_SCA_RICH_HELP_PANEL = 'SCA options'
7+
8+
NoRestoreOption = Annotated[
9+
bool,
10+
typer.Option(
11+
'--no-restore',
12+
help='When specified, Cycode will not run restore command. Will scan direct dependencies [b]only[/]!',
13+
rich_help_panel=_SCA_RICH_HELP_PANEL,
14+
),
15+
]
16+
17+
GradleAllSubProjectsOption = Annotated[
18+
bool,
19+
typer.Option(
20+
'--gradle-all-sub-projects',
21+
help='When specified, Cycode will run gradle restore command for all sub projects. '
22+
'Should run from root project directory [b]only[/]!',
23+
rich_help_panel=_SCA_RICH_HELP_PANEL,
24+
),
25+
]
26+
27+
MavenSettingsFileOption = Annotated[
28+
Optional[Path],
29+
typer.Option(
30+
'--maven-settings-file',
31+
show_default=False,
32+
help='When specified, Cycode will use this settings.xml file when building the maven dependency tree.',
33+
dir_okay=False,
34+
rich_help_panel=_SCA_RICH_HELP_PANEL,
35+
),
36+
]
37+
38+
39+
def apply_sca_restore_options_to_context(
40+
ctx: typer.Context,
41+
no_restore: bool,
42+
gradle_all_sub_projects: bool,
43+
maven_settings_file: Optional[Path],
44+
) -> None:
45+
ctx.obj['no_restore'] = no_restore
46+
ctx.obj['gradle_all_sub_projects'] = gradle_all_sub_projects
47+
ctx.obj['maven_settings_file'] = maven_settings_file

cycode/cli/apps/scan/scan_command.py

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
import click
66
import typer
77

8+
from cycode.cli.apps.sca_options import (
9+
GradleAllSubProjectsOption,
10+
MavenSettingsFileOption,
11+
NoRestoreOption,
12+
apply_sca_restore_options_to_context,
13+
)
814
from cycode.cli.apps.scan.remote_url_resolver import _try_get_git_remote_url
915
from cycode.cli.cli_types import ExportTypeOption, ScanTypeOption, ScaScanTypeOption, SeverityOption
1016
from cycode.cli.consts import (
@@ -72,33 +78,9 @@ def scan_command(
7278
rich_help_panel=_SCA_RICH_HELP_PANEL,
7379
),
7480
] = False,
75-
no_restore: Annotated[
76-
bool,
77-
typer.Option(
78-
'--no-restore',
79-
help='When specified, Cycode will not run restore command. Will scan direct dependencies [b]only[/]!',
80-
rich_help_panel=_SCA_RICH_HELP_PANEL,
81-
),
82-
] = False,
83-
gradle_all_sub_projects: Annotated[
84-
bool,
85-
typer.Option(
86-
'--gradle-all-sub-projects',
87-
help='When specified, Cycode will run gradle restore command for all sub projects. '
88-
'Should run from root project directory [b]only[/]!',
89-
rich_help_panel=_SCA_RICH_HELP_PANEL,
90-
),
91-
] = False,
92-
maven_settings_file: Annotated[
93-
Optional[Path],
94-
typer.Option(
95-
'--maven-settings-file',
96-
show_default=False,
97-
help='When specified, Cycode will use this settings.xml file when building the maven dependency tree.',
98-
dir_okay=False,
99-
rich_help_panel=_SCA_RICH_HELP_PANEL,
100-
),
101-
] = None,
81+
no_restore: NoRestoreOption = False,
82+
gradle_all_sub_projects: GradleAllSubProjectsOption = False,
83+
maven_settings_file: MavenSettingsFileOption = None,
10284
export_type: Annotated[
10385
ExportTypeOption,
10486
typer.Option(
@@ -152,10 +134,8 @@ def scan_command(
152134
ctx.obj['sync'] = sync
153135
ctx.obj['severity_threshold'] = severity_threshold
154136
ctx.obj['monitor'] = monitor
155-
ctx.obj['maven_settings_file'] = maven_settings_file
156137
ctx.obj['report'] = report
157-
ctx.obj['gradle_all_sub_projects'] = gradle_all_sub_projects
158-
ctx.obj['no_restore'] = no_restore
138+
apply_sca_restore_options_to_context(ctx, no_restore, gradle_all_sub_projects, maven_settings_file)
159139

160140
scan_client = get_scan_cycode_client(ctx)
161141
ctx.obj['client'] = scan_client

cycode/cli/cli_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class SbomFormatOption(StrEnum):
4646
SPDX_2_2 = 'spdx-2.2'
4747
SPDX_2_3 = 'spdx-2.3'
4848
CYCLONEDX_1_4 = 'cyclonedx-1.4'
49+
CYCLONEDX_1_6 = 'cyclonedx-1.6'
4950

5051

5152
class SbomOutputFormatOption(StrEnum):

cycode/cli/files_collector/sca/base_restore_dependencies.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import os
21
from abc import ABC, abstractmethod
2+
from pathlib import Path
33
from typing import Optional
44

55
import typer
@@ -32,6 +32,9 @@ def execute_commands(
3232
},
3333
)
3434

35+
if not commands:
36+
return None
37+
3538
try:
3639
outputs = []
3740

@@ -106,22 +109,43 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]:
106109
)
107110
return Document(relative_restore_file_path, restore_file_content, self.is_git_diff)
108111

112+
def get_manifest_dir(self, document: Document) -> Optional[str]:
113+
"""Return the directory containing the manifest file, resolving monitor-mode paths.
114+
115+
Uses the same path resolution as get_manifest_file_path() to ensure consistency.
116+
Falls back to document.absolute_path when the resolved manifest path is ambiguous.
117+
"""
118+
manifest_file_path = self.get_manifest_file_path(document)
119+
if manifest_file_path:
120+
parent = Path(manifest_file_path).parent
121+
# Skip '.' (no parent) and filesystem root (its own parent)
122+
if parent != Path('.') and parent != parent.parent:
123+
return str(parent)
124+
125+
base = document.absolute_path or document.path
126+
if base:
127+
parent = Path(base).parent
128+
if parent != Path('.') and parent != parent.parent:
129+
return str(parent)
130+
131+
return None
132+
109133
def get_working_directory(self, document: Document) -> Optional[str]:
110-
return os.path.dirname(document.absolute_path)
134+
return str(Path(document.absolute_path).parent)
111135

112136
def get_restored_lock_file_name(self, restore_file_path: str) -> str:
113137
return self.get_lock_file_name()
114138

115139
def get_any_restore_file_already_exist(self, document: Document, restore_file_paths: list[str]) -> str:
116140
for restore_file_path in restore_file_paths:
117-
if os.path.isfile(restore_file_path):
141+
if Path(restore_file_path).is_file():
118142
return restore_file_path
119143

120144
return build_dep_tree_path(document.absolute_path, self.get_lock_file_name())
121145

122146
@staticmethod
123147
def verify_restore_file_already_exist(restore_file_path: str) -> bool:
124-
return os.path.isfile(restore_file_path)
148+
return Path(restore_file_path).is_file()
125149

126150
@abstractmethod
127151
def is_project(self, document: Document) -> bool:

cycode/cli/files_collector/sca/go/restore_go_dependencies.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os
1+
from pathlib import Path
22
from typing import Optional
33

44
import typer
@@ -20,13 +20,13 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int)
2020
super().__init__(ctx, is_git_diff, command_timeout, create_output_file_manually=True)
2121

2222
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
23-
manifest_exists = os.path.isfile(self.get_working_directory(document) + os.sep + BUILD_GO_FILE_NAME)
24-
lock_exists = os.path.isfile(self.get_working_directory(document) + os.sep + BUILD_GO_LOCK_FILE_NAME)
23+
manifest_exists = (Path(self.get_working_directory(document)) / BUILD_GO_FILE_NAME).is_file()
24+
lock_exists = (Path(self.get_working_directory(document)) / BUILD_GO_LOCK_FILE_NAME).is_file()
2525

2626
if not manifest_exists or not lock_exists:
2727
logger.info('No manifest go.mod file found' if not manifest_exists else 'No manifest go.sum file found')
2828

29-
manifest_files_exists = manifest_exists & lock_exists
29+
manifest_files_exists = manifest_exists and lock_exists
3030

3131
if not manifest_files_exists:
3232
return None
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pathlib import Path
2+
from typing import Optional
3+
4+
import typer
5+
6+
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path
7+
from cycode.cli.models import Document
8+
from cycode.cli.utils.path_utils import get_file_content
9+
from cycode.logger import get_logger
10+
11+
logger = get_logger('Deno Restore Dependencies')
12+
13+
DENO_MANIFEST_FILE_NAMES = ('deno.json', 'deno.jsonc')
14+
DENO_LOCK_FILE_NAME = 'deno.lock'
15+
16+
17+
class RestoreDenoDependencies(BaseRestoreDependencies):
18+
def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) -> None:
19+
super().__init__(ctx, is_git_diff, command_timeout)
20+
21+
def is_project(self, document: Document) -> bool:
22+
return Path(document.path).name in DENO_MANIFEST_FILE_NAMES
23+
24+
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
25+
manifest_dir = self.get_manifest_dir(document)
26+
if not manifest_dir:
27+
return None
28+
29+
lockfile_path = Path(manifest_dir) / DENO_LOCK_FILE_NAME
30+
if not lockfile_path.is_file():
31+
logger.debug('No deno.lock found alongside deno.json, skipping deno restore, %s', {'path': document.path})
32+
return None
33+
34+
content = get_file_content(str(lockfile_path))
35+
relative_path = build_dep_tree_path(document.path, DENO_LOCK_FILE_NAME)
36+
logger.debug('Using existing deno.lock, %s', {'path': str(lockfile_path)})
37+
return Document(relative_path, content, self.is_git_diff)
38+
39+
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
40+
return []
41+
42+
def get_lock_file_name(self) -> str:
43+
return DENO_LOCK_FILE_NAME
44+
45+
def get_lock_file_names(self) -> list[str]:
46+
return [DENO_LOCK_FILE_NAME]

0 commit comments

Comments
 (0)