Skip to content

Commit f6c6543

Browse files
authored
CM-40907 - Implement NPM restore support for SCA (#254)
1 parent a92f638 commit f6c6543

File tree

7 files changed

+63
-15
lines changed

7 files changed

+63
-15
lines changed

cycode/cli/files_collector/sca/base_restore_dependencies.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ def build_dep_tree_path(path: str, generated_file_name: str) -> str:
1313
return join_paths(get_file_dir(path), generated_file_name)
1414

1515

16-
def execute_command(command: List[str], file_name: str, command_timeout: int) -> Optional[str]:
16+
def execute_command(
17+
command: List[str], file_name: str, command_timeout: int, dependencies_file_name: Optional[str] = None
18+
) -> Optional[str]:
1719
try:
18-
dependencies = shell(command, command_timeout)
20+
dependencies = shell(command=command, timeout=command_timeout)
21+
# Write stdout output to the file if output_file_path is provided
22+
if dependencies_file_name:
23+
with open(dependencies_file_name, 'w') as output_file:
24+
output_file.write(dependencies)
1925
except Exception as e:
2026
logger.debug('Failed to restore dependencies via shell command, %s', {'filename': file_name}, exc_info=e)
2127
return None
@@ -24,10 +30,13 @@ def execute_command(command: List[str], file_name: str, command_timeout: int) ->
2430

2531

2632
class BaseRestoreDependencies(ABC):
27-
def __init__(self, context: click.Context, is_git_diff: bool, command_timeout: int) -> None:
33+
def __init__(
34+
self, context: click.Context, is_git_diff: bool, command_timeout: int, create_output_file_manually: bool = False
35+
) -> None:
2836
self.context = context
2937
self.is_git_diff = is_git_diff
3038
self.command_timeout = command_timeout
39+
self.create_output_file_manually = create_output_file_manually
3140

3241
def restore(self, document: Document) -> Optional[Document]:
3342
return self.try_restore_dependencies(document)
@@ -46,9 +55,11 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]:
4655
if self.verify_restore_file_already_exist(restore_file_path):
4756
restore_file_content = get_file_content(restore_file_path)
4857
else:
49-
restore_file_content = execute_command(
50-
self.get_command(manifest_file_path), manifest_file_path, self.command_timeout
58+
output_file_path = restore_file_path if self.create_output_file_manually else None
59+
execute_command(
60+
self.get_command(manifest_file_path), manifest_file_path, self.command_timeout, output_file_path
5161
)
62+
restore_file_content = get_file_content(restore_file_path)
5263

5364
return Document(restore_file_path, restore_file_content, self.is_git_diff)
5465

cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
class RestoreGradleDependencies(BaseRestoreDependencies):
1515
def __init__(self, context: click.Context, is_git_diff: bool, command_timeout: int) -> None:
16-
super().__init__(context, is_git_diff, command_timeout)
16+
super().__init__(context, is_git_diff, command_timeout, create_output_file_manually=True)
1717

1818
def is_project(self, document: Document) -> bool:
1919
return document.path.endswith(BUILD_GRADLE_FILE_NAME) or document.path.endswith(BUILD_GRADLE_KTS_FILE_NAME)

cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from os import path
23
from typing import List, Optional
34

@@ -30,7 +31,7 @@ def get_lock_file_name(self) -> str:
3031
return join_paths('target', MAVEN_CYCLONE_DEP_TREE_FILE_NAME)
3132

3233
def verify_restore_file_already_exist(self, restore_file_path: str) -> bool:
33-
return False
34+
return os.path.isfile(restore_file_path)
3435

3536
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
3637
restore_dependencies_document = super().try_restore_dependencies(document)

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

Whitespace-only changes.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
from typing import List
3+
4+
import click
5+
6+
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies
7+
from cycode.cli.models import Document
8+
9+
NPM_PROJECT_FILE_EXTENSIONS = ['.json']
10+
NPM_LOCK_FILE_NAME = 'package-lock.json'
11+
NPM_MANIFEST_FILE_NAME = 'package.json'
12+
13+
14+
class RestoreNpmDependencies(BaseRestoreDependencies):
15+
def __init__(self, context: click.Context, is_git_diff: bool, command_timeout: int) -> None:
16+
super().__init__(context, is_git_diff, command_timeout)
17+
18+
def is_project(self, document: Document) -> bool:
19+
return any(document.path.endswith(ext) for ext in NPM_PROJECT_FILE_EXTENSIONS)
20+
21+
def get_command(self, manifest_file_path: str) -> List[str]:
22+
return [
23+
'npm',
24+
'install',
25+
'--prefix',
26+
self.prepare_manifest_file_path_for_command(manifest_file_path),
27+
'--package-lock-only',
28+
'--ignore-scripts',
29+
'--no-audit',
30+
]
31+
32+
def get_lock_file_name(self) -> str:
33+
return NPM_LOCK_FILE_NAME
34+
35+
def verify_restore_file_already_exist(self, restore_file_path: str) -> bool:
36+
return os.path.isfile(restore_file_path)
37+
38+
def prepare_manifest_file_path_for_command(self, manifest_file_path: str) -> str:
39+
return manifest_file_path.replace(os.sep + NPM_MANIFEST_FILE_NAME, '')

cycode/cli/files_collector/sca/sca_code_scanner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +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
1011
from cycode.cli.files_collector.sca.nuget.restore_nuget_dependencies import RestoreNugetDependencies
1112
from cycode.cli.models import Document
1213
from cycode.cli.utils.git_proxy import git_proxy
@@ -18,6 +19,7 @@
1819

1920
BUILD_GRADLE_DEP_TREE_TIMEOUT = 180
2021
BUILD_NUGET_DEP_TREE_TIMEOUT = 180
22+
BUILD_NPM_DEP_TREE_TIMEOUT = 180
2123

2224

2325
def perform_pre_commit_range_scan_actions(
@@ -132,6 +134,7 @@ def restore_handlers(context: click.Context, is_git_diff: bool) -> List[BaseRest
132134
RestoreGradleDependencies(context, is_git_diff, BUILD_GRADLE_DEP_TREE_TIMEOUT),
133135
RestoreMavenDependencies(context, is_git_diff, BUILD_GRADLE_DEP_TREE_TIMEOUT),
134136
RestoreNugetDependencies(context, is_git_diff, BUILD_NUGET_DEP_TREE_TIMEOUT),
137+
RestoreNpmDependencies(context, is_git_diff, BUILD_NPM_DEP_TREE_TIMEOUT),
135138
]
136139

137140

cycode/cli/utils/shell_executor.py

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

1010

11-
def shell(
12-
command: Union[str, List[str]], timeout: int = _SUBPROCESS_DEFAULT_TIMEOUT_SEC, execute_in_shell: bool = False
13-
) -> Optional[str]:
11+
def shell(command: Union[str, List[str]], timeout: int = _SUBPROCESS_DEFAULT_TIMEOUT_SEC) -> Optional[str]:
1412
logger.debug('Executing shell command: %s', command)
1513

1614
try:
1715
result = subprocess.run( # noqa: S603
18-
command,
19-
timeout=timeout,
20-
shell=execute_in_shell,
21-
check=True,
22-
capture_output=True,
16+
command, timeout=timeout, check=True, capture_output=True
2317
)
2418

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

0 commit comments

Comments
 (0)