Skip to content

Commit 84b298c

Browse files
authored
CM-41217 - Implement SBT restore support for SCA (#259)
1 parent bb22262 commit 84b298c

File tree

8 files changed

+80
-17
lines changed

8 files changed

+80
-17
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ This guide will guide you through both installation and usage.
3030
6. [Commit History Scan](#commit-history-scan)
3131
1. [Commit Range Option](#commit-range-option)
3232
7. [Pre-Commit Scan](#pre-commit-scan)
33+
8. [Lock Restore Options](#lock-restore-options)
34+
1. [SBT Scan](#sbt-scan)
3335
2. [Scan Results](#scan-results)
3436
1. [Show/Hide Secrets](#showhide-secrets)
3537
2. [Soft Fail](#soft-fail)
@@ -496,6 +498,17 @@ After your install the pre-commit hook and, you may, on occasion, wish to skip s
496498
497499
`SKIP=cycode git commit -m <your commit message>`
498500
501+
### Lock Restore Options
502+
503+
#### SBT Scan
504+
505+
We use sbt-dependency-lock plugin to restore the lock file for SBT projects.
506+
To disable lock restore in use `--no-restore` option.
507+
508+
Prerequisites
509+
* sbt-dependency-lock Plugin: Install the plugin by adding the following line to `project/plugins.sbt`:
510+
`addSbtPlugin("software.purpledragon" % "sbt-dependency-lock" % "1.5.1")`
511+
499512
## Scan Results
500513
501514
Each scan will complete with a message stating if any issues were found or not.

cycode/cli/commands/scan/repository/repository_command.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,15 @@ def repository_command(context: click.Context, path: str, branch: str) -> None:
4848
# FIXME(MarshalX): probably file could be tree or submodule too. we expect blob only
4949
progress_bar.update(ScanProgressBarSection.PREPARE_LOCAL_FILES)
5050

51-
file_path = file.path if monitor else get_path_by_os(os.path.join(path, file.path))
52-
documents_to_scan.append(Document(file_path, file.data_stream.read().decode('UTF-8', errors='replace')))
51+
absolute_path = get_path_by_os(os.path.join(path, file.path))
52+
file_path = file.path if monitor else absolute_path
53+
documents_to_scan.append(
54+
Document(
55+
file_path,
56+
file.data_stream.read().decode('UTF-8', errors='replace'),
57+
absolute_path=absolute_path,
58+
)
59+
)
5360

5461
documents_to_scan = exclude_irrelevant_documents_to_scan(scan_type, documents_to_scan)
5562

cycode/cli/files_collector/sca/base_restore_dependencies.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ def build_dep_tree_path(path: str, generated_file_name: str) -> str:
1414

1515

1616
def execute_command(
17-
command: List[str], file_name: str, command_timeout: int, dependencies_file_name: Optional[str] = None
17+
command: List[str],
18+
file_name: str,
19+
command_timeout: int,
20+
dependencies_file_name: Optional[str] = None,
21+
working_directory: Optional[str] = None,
1822
) -> Optional[str]:
1923
try:
20-
dependencies = shell(command=command, timeout=command_timeout)
24+
dependencies = shell(command=command, timeout=command_timeout, working_directory=working_directory)
2125
# Write stdout output to the file if output_file_path is provided
2226
if dependencies_file_name:
2327
with open(dependencies_file_name, 'w') as output_file:
@@ -51,18 +55,26 @@ def get_manifest_file_path(self, document: Document) -> str:
5155
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
5256
manifest_file_path = self.get_manifest_file_path(document)
5357
restore_file_path = build_dep_tree_path(document.path, self.get_lock_file_name())
58+
working_directory_path = self.get_working_directory(document)
5459

5560
if self.verify_restore_file_already_exist(restore_file_path):
5661
restore_file_content = get_file_content(restore_file_path)
5762
else:
5863
output_file_path = restore_file_path if self.create_output_file_manually else None
5964
execute_command(
60-
self.get_command(manifest_file_path), manifest_file_path, self.command_timeout, output_file_path
65+
self.get_command(manifest_file_path),
66+
manifest_file_path,
67+
self.command_timeout,
68+
output_file_path,
69+
working_directory_path,
6170
)
6271
restore_file_content = get_file_content(restore_file_path)
6372

6473
return Document(restore_file_path, restore_file_content, self.is_git_diff)
6574

75+
def get_working_directory(self, document: Document) -> Optional[str]:
76+
return None
77+
6678
@abstractmethod
6779
def verify_restore_file_already_exist(self, restore_file_path: str) -> bool:
6880
pass

cycode/cli/files_collector/sca/sbt/__init__.py

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import os
2+
from typing import List, Optional
3+
4+
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies
5+
from cycode.cli.models import Document
6+
7+
SBT_PROJECT_FILE_EXTENSIONS = ['sbt']
8+
SBT_LOCK_FILE_NAME = 'build.sbt.lock'
9+
10+
11+
class RestoreSbtDependencies(BaseRestoreDependencies):
12+
def is_project(self, document: Document) -> bool:
13+
return any(document.path.endswith(ext) for ext in SBT_PROJECT_FILE_EXTENSIONS)
14+
15+
def get_command(self, manifest_file_path: str) -> List[str]:
16+
return ['sbt', 'dependencyLockWrite', '--verbose']
17+
18+
def get_lock_file_name(self) -> str:
19+
return SBT_LOCK_FILE_NAME
20+
21+
def verify_restore_file_already_exist(self, restore_file_path: str) -> bool:
22+
return os.path.isfile(restore_file_path)
23+
24+
def get_working_directory(self, document: Document) -> Optional[str]:
25+
return os.path.dirname(document.absolute_path)

cycode/cli/files_collector/sca/sca_code_scanner.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies
88
from cycode.cli.files_collector.sca.maven.restore_gradle_dependencies import RestoreGradleDependencies
99
from cycode.cli.files_collector.sca.maven.restore_maven_dependencies import RestoreMavenDependencies
10-
from cycode.cli.files_collector.sca.npm.restore_npm_dependencies import RestoreNpmDependencies
11-
from cycode.cli.files_collector.sca.nuget.restore_nuget_dependencies import RestoreNugetDependencies
10+
from cycode.cli.files_collector.sca.sbt.restore_sbt_dependencies import RestoreSbtDependencies
1211
from cycode.cli.models import Document
1312
from cycode.cli.utils.git_proxy import git_proxy
1413
from cycode.cli.utils.path_utils import get_file_content, get_file_dir, get_path_from_context, join_paths
@@ -17,9 +16,7 @@
1716
if TYPE_CHECKING:
1817
from git import Repo
1918

20-
BUILD_GRADLE_DEP_TREE_TIMEOUT = 180
21-
BUILD_NUGET_DEP_TREE_TIMEOUT = 180
22-
BUILD_NPM_DEP_TREE_TIMEOUT = 180
19+
BUILD_DEP_TREE_TIMEOUT = 180
2320

2421

2522
def perform_pre_commit_range_scan_actions(
@@ -132,10 +129,9 @@ def add_dependencies_tree_document(
132129

133130
def restore_handlers(context: click.Context, is_git_diff: bool) -> List[BaseRestoreDependencies]:
134131
return [
135-
RestoreGradleDependencies(context, is_git_diff, BUILD_GRADLE_DEP_TREE_TIMEOUT),
136-
RestoreMavenDependencies(context, is_git_diff, BUILD_GRADLE_DEP_TREE_TIMEOUT),
137-
RestoreNugetDependencies(context, is_git_diff, BUILD_NUGET_DEP_TREE_TIMEOUT),
138-
RestoreNpmDependencies(context, is_git_diff, BUILD_NPM_DEP_TREE_TIMEOUT),
132+
RestoreGradleDependencies(context, is_git_diff, BUILD_DEP_TREE_TIMEOUT),
133+
RestoreMavenDependencies(context, is_git_diff, BUILD_DEP_TREE_TIMEOUT),
134+
RestoreSbtDependencies(context, is_git_diff, BUILD_DEP_TREE_TIMEOUT),
139135
]
140136

141137

cycode/cli/models.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77

88
class Document:
99
def __init__(
10-
self, path: str, content: str, is_git_diff_format: bool = False, unique_id: Optional[str] = None
10+
self,
11+
path: str,
12+
content: str,
13+
is_git_diff_format: bool = False,
14+
unique_id: Optional[str] = None,
15+
absolute_path: Optional[str] = None,
1116
) -> None:
1217
self.path = path
1318
self.content = content
1419
self.is_git_diff_format = is_git_diff_format
1520
self.unique_id = unique_id
21+
self.absolute_path = absolute_path
1622

1723
def __repr__(self) -> str:
1824
return 'path:{0}, content:{1}'.format(self.path, self.content)

cycode/cli/utils/shell_executor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
_SUBPROCESS_DEFAULT_TIMEOUT_SEC = 60
99

1010

11-
def shell(command: Union[str, List[str]], timeout: int = _SUBPROCESS_DEFAULT_TIMEOUT_SEC) -> Optional[str]:
11+
def shell(
12+
command: Union[str, List[str]],
13+
timeout: int = _SUBPROCESS_DEFAULT_TIMEOUT_SEC,
14+
working_directory: Optional[str] = None,
15+
) -> Optional[str]:
1216
logger.debug('Executing shell command: %s', command)
1317

1418
try:
1519
result = subprocess.run( # noqa: S603
16-
command, timeout=timeout, check=True, capture_output=True
20+
command, cwd=working_directory, timeout=timeout, check=True, capture_output=True
1721
)
1822

1923
return result.stdout.decode('UTF-8').strip()

0 commit comments

Comments
 (0)