cycode 3.10.3.dev1__py3-none-any.whl → 3.10.4.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cycode/__init__.py +1 -1
- cycode/cli/apps/report/sbom/path/path_command.py +11 -14
- cycode/cli/apps/sca_options.py +47 -0
- cycode/cli/apps/scan/code_scanner.py +76 -4
- cycode/cli/apps/scan/commit_range_scanner.py +51 -8
- cycode/cli/apps/scan/scan_command.py +10 -30
- cycode/cli/cli_types.py +1 -0
- cycode/cli/consts.py +5 -2
- cycode/cli/files_collector/sca/base_restore_dependencies.py +28 -4
- cycode/cli/files_collector/sca/go/restore_go_dependencies.py +4 -4
- cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +46 -0
- cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +23 -136
- cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +70 -0
- cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +70 -0
- cycode/cli/files_collector/sca/php/__init__.py +0 -0
- cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +54 -0
- cycode/cli/files_collector/sca/python/__init__.py +0 -0
- cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +45 -0
- cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +62 -0
- cycode/cli/files_collector/sca/sca_file_collector.py +13 -1
- cycode/cli/files_collector/zip_documents.py +5 -1
- cycode/cli/utils/scan_batch.py +5 -1
- cycode/cli/utils/scan_utils.py +5 -0
- cycode/cyclient/models.py +20 -0
- cycode/cyclient/scan_client.py +61 -0
- {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/METADATA +31 -11
- {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/RECORD +30 -21
- {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/WHEEL +0 -0
- {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/entry_points.txt +0 -0
- {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/licenses/LICENCE +0 -0
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import Optional
|
|
1
|
+
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
import typer
|
|
5
4
|
|
|
6
|
-
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies
|
|
5
|
+
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies
|
|
7
6
|
from cycode.cli.models import Document
|
|
8
|
-
from cycode.cli.utils.path_utils import get_file_content
|
|
9
7
|
from cycode.logger import get_logger
|
|
10
8
|
|
|
11
9
|
logger = get_logger('NPM Restore Dependencies')
|
|
12
10
|
|
|
13
|
-
NPM_PROJECT_FILE_EXTENSIONS = ['.json']
|
|
14
|
-
NPM_LOCK_FILE_NAME = 'package-lock.json'
|
|
15
|
-
# Alternative lockfiles that should prevent npm install from running
|
|
16
|
-
ALTERNATIVE_LOCK_FILES = ['yarn.lock', 'pnpm-lock.yaml', 'deno.lock']
|
|
17
|
-
NPM_LOCK_FILE_NAMES = [NPM_LOCK_FILE_NAME, *ALTERNATIVE_LOCK_FILES]
|
|
18
11
|
NPM_MANIFEST_FILE_NAME = 'package.json'
|
|
12
|
+
NPM_LOCK_FILE_NAME = 'package-lock.json'
|
|
13
|
+
# These lockfiles indicate another package manager owns the project — NPM should not run
|
|
14
|
+
_ALTERNATIVE_LOCK_FILES = ('yarn.lock', 'pnpm-lock.yaml', 'deno.lock')
|
|
19
15
|
|
|
20
16
|
|
|
21
17
|
class RestoreNpmDependencies(BaseRestoreDependencies):
|
|
@@ -23,128 +19,25 @@ class RestoreNpmDependencies(BaseRestoreDependencies):
|
|
|
23
19
|
super().__init__(ctx, is_git_diff, command_timeout)
|
|
24
20
|
|
|
25
21
|
def is_project(self, document: Document) -> bool:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _resolve_manifest_directory(self, document: Document) -> Optional[str]:
|
|
29
|
-
"""Resolve the directory containing the manifest file.
|
|
30
|
-
|
|
31
|
-
Uses the same path resolution logic as get_manifest_file_path() to ensure consistency.
|
|
32
|
-
Falls back to absolute_path or document.path if needed.
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
Directory path if resolved, None otherwise.
|
|
36
|
-
"""
|
|
37
|
-
manifest_file_path = self.get_manifest_file_path(document)
|
|
38
|
-
manifest_dir = os.path.dirname(manifest_file_path) if manifest_file_path else None
|
|
39
|
-
|
|
40
|
-
# Fallback: if manifest_dir is empty or root, try using absolute_path or document.path
|
|
41
|
-
if not manifest_dir or manifest_dir == os.sep or manifest_dir == '.':
|
|
42
|
-
base_path = document.absolute_path if document.absolute_path else document.path
|
|
43
|
-
if base_path:
|
|
44
|
-
manifest_dir = os.path.dirname(base_path)
|
|
22
|
+
"""Match only package.json files that are not managed by Yarn or pnpm.
|
|
45
23
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _find_existing_lockfile(self, manifest_dir: str) -> tuple[Optional[str], list[str]]:
|
|
49
|
-
"""Find the first existing lockfile in the manifest directory.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
manifest_dir: Directory to search for lockfiles.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Tuple of (lockfile_path if found, list of checked lockfiles with status).
|
|
24
|
+
Yarn and pnpm projects are handled by their dedicated handlers, which run before
|
|
25
|
+
this one in the handler list. This handler is the npm fallback.
|
|
56
26
|
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
existing_lock_file = None
|
|
60
|
-
checked_lockfiles = []
|
|
61
|
-
for lock_file_path in lock_file_paths:
|
|
62
|
-
lock_file_name = os.path.basename(lock_file_path)
|
|
63
|
-
exists = os.path.isfile(lock_file_path)
|
|
64
|
-
checked_lockfiles.append(f'{lock_file_name}: {"exists" if exists else "not found"}')
|
|
65
|
-
if exists:
|
|
66
|
-
existing_lock_file = lock_file_path
|
|
67
|
-
break
|
|
27
|
+
if Path(document.path).name != NPM_MANIFEST_FILE_NAME:
|
|
28
|
+
return False
|
|
68
29
|
|
|
69
|
-
|
|
30
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
31
|
+
if manifest_dir:
|
|
32
|
+
for lock_file in _ALTERNATIVE_LOCK_FILES:
|
|
33
|
+
if (Path(manifest_dir) / lock_file).is_file():
|
|
34
|
+
logger.debug(
|
|
35
|
+
'Skipping npm restore: alternative lockfile detected, %s',
|
|
36
|
+
{'path': document.path, 'lockfile': lock_file},
|
|
37
|
+
)
|
|
38
|
+
return False
|
|
70
39
|
|
|
71
|
-
|
|
72
|
-
"""Create a Document from an existing lockfile.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
document: Original document (package.json).
|
|
76
|
-
lockfile_path: Path to the existing lockfile.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
Document with lockfile content if successful, None otherwise.
|
|
80
|
-
"""
|
|
81
|
-
lock_file_name = os.path.basename(lockfile_path)
|
|
82
|
-
logger.info(
|
|
83
|
-
'Skipping npm install: using existing lockfile, %s',
|
|
84
|
-
{'path': document.path, 'lockfile': lock_file_name, 'lockfile_path': lockfile_path},
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
relative_restore_file_path = build_dep_tree_path(document.path, lock_file_name)
|
|
88
|
-
restore_file_content = get_file_content(lockfile_path)
|
|
89
|
-
|
|
90
|
-
if restore_file_content is not None:
|
|
91
|
-
logger.debug(
|
|
92
|
-
'Successfully loaded lockfile content, %s',
|
|
93
|
-
{'path': document.path, 'lockfile': lock_file_name, 'content_size': len(restore_file_content)},
|
|
94
|
-
)
|
|
95
|
-
return Document(relative_restore_file_path, restore_file_content, self.is_git_diff)
|
|
96
|
-
|
|
97
|
-
logger.warning(
|
|
98
|
-
'Lockfile exists but could not read content, %s',
|
|
99
|
-
{'path': document.path, 'lockfile': lock_file_name, 'lockfile_path': lockfile_path},
|
|
100
|
-
)
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
|
|
104
|
-
"""Override to prevent npm install when any lockfile exists.
|
|
105
|
-
|
|
106
|
-
The base class uses document.absolute_path which might be None or incorrect.
|
|
107
|
-
We need to use the same path resolution logic as get_manifest_file_path()
|
|
108
|
-
to ensure we check for lockfiles in the correct location.
|
|
109
|
-
|
|
110
|
-
If any lockfile exists (package-lock.json, pnpm-lock.yaml, yarn.lock, deno.lock),
|
|
111
|
-
we use it directly without running npm install to avoid generating invalid lockfiles.
|
|
112
|
-
"""
|
|
113
|
-
# Check if this is a project file first (same as base class caller does)
|
|
114
|
-
if not self.is_project(document):
|
|
115
|
-
logger.debug('Skipping restore: document is not recognized as npm project, %s', {'path': document.path})
|
|
116
|
-
return None
|
|
117
|
-
|
|
118
|
-
# Resolve the manifest directory
|
|
119
|
-
manifest_dir = self._resolve_manifest_directory(document)
|
|
120
|
-
if not manifest_dir:
|
|
121
|
-
logger.debug(
|
|
122
|
-
'Cannot determine manifest directory, proceeding with base class restore flow, %s',
|
|
123
|
-
{'path': document.path},
|
|
124
|
-
)
|
|
125
|
-
return super().try_restore_dependencies(document)
|
|
126
|
-
|
|
127
|
-
# Check for existing lockfiles
|
|
128
|
-
logger.debug(
|
|
129
|
-
'Checking for existing lockfiles in directory, %s', {'directory': manifest_dir, 'path': document.path}
|
|
130
|
-
)
|
|
131
|
-
existing_lock_file, checked_lockfiles = self._find_existing_lockfile(manifest_dir)
|
|
132
|
-
|
|
133
|
-
logger.debug(
|
|
134
|
-
'Lockfile check results, %s',
|
|
135
|
-
{'path': document.path, 'checked_lockfiles': ', '.join(checked_lockfiles)},
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
# If any lockfile exists, use it directly without running npm install
|
|
139
|
-
if existing_lock_file:
|
|
140
|
-
return self._create_document_from_lockfile(document, existing_lock_file)
|
|
141
|
-
|
|
142
|
-
# No lockfile exists, proceed with the normal restore flow which will run npm install
|
|
143
|
-
logger.info(
|
|
144
|
-
'No existing lockfile found, proceeding with npm install to generate package-lock.json, %s',
|
|
145
|
-
{'path': document.path, 'directory': manifest_dir, 'checked_lockfiles': ', '.join(checked_lockfiles)},
|
|
146
|
-
)
|
|
147
|
-
return super().try_restore_dependencies(document)
|
|
40
|
+
return True
|
|
148
41
|
|
|
149
42
|
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
|
|
150
43
|
return [
|
|
@@ -159,22 +52,16 @@ class RestoreNpmDependencies(BaseRestoreDependencies):
|
|
|
159
52
|
]
|
|
160
53
|
]
|
|
161
54
|
|
|
162
|
-
def get_restored_lock_file_name(self, restore_file_path: str) -> str:
|
|
163
|
-
return os.path.basename(restore_file_path)
|
|
164
|
-
|
|
165
55
|
def get_lock_file_name(self) -> str:
|
|
166
56
|
return NPM_LOCK_FILE_NAME
|
|
167
57
|
|
|
168
58
|
def get_lock_file_names(self) -> list[str]:
|
|
169
|
-
return
|
|
59
|
+
return [NPM_LOCK_FILE_NAME]
|
|
170
60
|
|
|
171
61
|
@staticmethod
|
|
172
62
|
def prepare_manifest_file_path_for_command(manifest_file_path: str) -> str:
|
|
173
|
-
# Remove package.json from the path
|
|
174
63
|
if manifest_file_path.endswith(NPM_MANIFEST_FILE_NAME):
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
dir_path = os.path.dirname(manifest_file_path)
|
|
178
|
-
# If dir_path is empty or just '.', return an empty string (package.json in current dir)
|
|
64
|
+
parent = Path(manifest_file_path).parent
|
|
65
|
+
dir_path = str(parent)
|
|
179
66
|
return dir_path if dir_path and dir_path != '.' else ''
|
|
180
67
|
return manifest_file_path
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path
|
|
8
|
+
from cycode.cli.models import Document
|
|
9
|
+
from cycode.cli.utils.path_utils import get_file_content
|
|
10
|
+
from cycode.logger import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger('Pnpm Restore Dependencies')
|
|
13
|
+
|
|
14
|
+
PNPM_MANIFEST_FILE_NAME = 'package.json'
|
|
15
|
+
PNPM_LOCK_FILE_NAME = 'pnpm-lock.yaml'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _indicates_pnpm(package_json_content: Optional[str]) -> bool:
|
|
19
|
+
"""Return True if package.json content signals that this project uses pnpm."""
|
|
20
|
+
if not package_json_content:
|
|
21
|
+
return False
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(package_json_content)
|
|
24
|
+
except (json.JSONDecodeError, ValueError):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
package_manager = data.get('packageManager', '')
|
|
28
|
+
if isinstance(package_manager, str) and package_manager.startswith('pnpm'):
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
engines = data.get('engines', {})
|
|
32
|
+
return isinstance(engines, dict) and 'pnpm' in engines
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RestorePnpmDependencies(BaseRestoreDependencies):
|
|
36
|
+
def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) -> None:
|
|
37
|
+
super().__init__(ctx, is_git_diff, command_timeout)
|
|
38
|
+
|
|
39
|
+
def is_project(self, document: Document) -> bool:
|
|
40
|
+
if Path(document.path).name != PNPM_MANIFEST_FILE_NAME:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
44
|
+
if manifest_dir and (Path(manifest_dir) / PNPM_LOCK_FILE_NAME).is_file():
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
return _indicates_pnpm(document.content)
|
|
48
|
+
|
|
49
|
+
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
|
|
50
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
51
|
+
lockfile_path = Path(manifest_dir) / PNPM_LOCK_FILE_NAME if manifest_dir else None
|
|
52
|
+
|
|
53
|
+
if lockfile_path and lockfile_path.is_file():
|
|
54
|
+
# Lockfile already exists — read it directly without running pnpm
|
|
55
|
+
content = get_file_content(str(lockfile_path))
|
|
56
|
+
relative_path = build_dep_tree_path(document.path, PNPM_LOCK_FILE_NAME)
|
|
57
|
+
logger.debug('Using existing pnpm-lock.yaml, %s', {'path': str(lockfile_path)})
|
|
58
|
+
return Document(relative_path, content, self.is_git_diff)
|
|
59
|
+
|
|
60
|
+
# Lockfile absent but pnpm is indicated in package.json — generate it
|
|
61
|
+
return super().try_restore_dependencies(document)
|
|
62
|
+
|
|
63
|
+
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
|
|
64
|
+
return [['pnpm', 'install', '--ignore-scripts']]
|
|
65
|
+
|
|
66
|
+
def get_lock_file_name(self) -> str:
|
|
67
|
+
return PNPM_LOCK_FILE_NAME
|
|
68
|
+
|
|
69
|
+
def get_lock_file_names(self) -> list[str]:
|
|
70
|
+
return [PNPM_LOCK_FILE_NAME]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path
|
|
8
|
+
from cycode.cli.models import Document
|
|
9
|
+
from cycode.cli.utils.path_utils import get_file_content
|
|
10
|
+
from cycode.logger import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger('Yarn Restore Dependencies')
|
|
13
|
+
|
|
14
|
+
YARN_MANIFEST_FILE_NAME = 'package.json'
|
|
15
|
+
YARN_LOCK_FILE_NAME = 'yarn.lock'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _indicates_yarn(package_json_content: Optional[str]) -> bool:
|
|
19
|
+
"""Return True if package.json content signals that this project uses Yarn."""
|
|
20
|
+
if not package_json_content:
|
|
21
|
+
return False
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(package_json_content)
|
|
24
|
+
except (json.JSONDecodeError, ValueError):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
package_manager = data.get('packageManager', '')
|
|
28
|
+
if isinstance(package_manager, str) and package_manager.startswith('yarn'):
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
engines = data.get('engines', {})
|
|
32
|
+
return isinstance(engines, dict) and 'yarn' in engines
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RestoreYarnDependencies(BaseRestoreDependencies):
|
|
36
|
+
def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) -> None:
|
|
37
|
+
super().__init__(ctx, is_git_diff, command_timeout)
|
|
38
|
+
|
|
39
|
+
def is_project(self, document: Document) -> bool:
|
|
40
|
+
if Path(document.path).name != YARN_MANIFEST_FILE_NAME:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
44
|
+
if manifest_dir and (Path(manifest_dir) / YARN_LOCK_FILE_NAME).is_file():
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
return _indicates_yarn(document.content)
|
|
48
|
+
|
|
49
|
+
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
|
|
50
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
51
|
+
lockfile_path = Path(manifest_dir) / YARN_LOCK_FILE_NAME if manifest_dir else None
|
|
52
|
+
|
|
53
|
+
if lockfile_path and lockfile_path.is_file():
|
|
54
|
+
# Lockfile already exists — read it directly without running yarn
|
|
55
|
+
content = get_file_content(str(lockfile_path))
|
|
56
|
+
relative_path = build_dep_tree_path(document.path, YARN_LOCK_FILE_NAME)
|
|
57
|
+
logger.debug('Using existing yarn.lock, %s', {'path': str(lockfile_path)})
|
|
58
|
+
return Document(relative_path, content, self.is_git_diff)
|
|
59
|
+
|
|
60
|
+
# Lockfile absent but yarn is indicated in package.json — generate it
|
|
61
|
+
return super().try_restore_dependencies(document)
|
|
62
|
+
|
|
63
|
+
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
|
|
64
|
+
return [['yarn', 'install', '--ignore-scripts']]
|
|
65
|
+
|
|
66
|
+
def get_lock_file_name(self) -> str:
|
|
67
|
+
return YARN_LOCK_FILE_NAME
|
|
68
|
+
|
|
69
|
+
def get_lock_file_names(self) -> list[str]:
|
|
70
|
+
return [YARN_LOCK_FILE_NAME]
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
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('Composer Restore Dependencies')
|
|
12
|
+
|
|
13
|
+
COMPOSER_MANIFEST_FILE_NAME = 'composer.json'
|
|
14
|
+
COMPOSER_LOCK_FILE_NAME = 'composer.lock'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RestoreComposerDependencies(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 == COMPOSER_MANIFEST_FILE_NAME
|
|
23
|
+
|
|
24
|
+
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
|
|
25
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
26
|
+
lockfile_path = Path(manifest_dir) / COMPOSER_LOCK_FILE_NAME if manifest_dir else None
|
|
27
|
+
|
|
28
|
+
if lockfile_path and lockfile_path.is_file():
|
|
29
|
+
# Lockfile already exists — read it directly without running composer
|
|
30
|
+
content = get_file_content(str(lockfile_path))
|
|
31
|
+
relative_path = build_dep_tree_path(document.path, COMPOSER_LOCK_FILE_NAME)
|
|
32
|
+
logger.debug('Using existing composer.lock, %s', {'path': str(lockfile_path)})
|
|
33
|
+
return Document(relative_path, content, self.is_git_diff)
|
|
34
|
+
|
|
35
|
+
# Lockfile absent — generate it
|
|
36
|
+
return super().try_restore_dependencies(document)
|
|
37
|
+
|
|
38
|
+
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
|
|
39
|
+
return [
|
|
40
|
+
[
|
|
41
|
+
'composer',
|
|
42
|
+
'update',
|
|
43
|
+
'--no-cache',
|
|
44
|
+
'--no-install',
|
|
45
|
+
'--no-scripts',
|
|
46
|
+
'--ignore-platform-reqs',
|
|
47
|
+
]
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def get_lock_file_name(self) -> str:
|
|
51
|
+
return COMPOSER_LOCK_FILE_NAME
|
|
52
|
+
|
|
53
|
+
def get_lock_file_names(self) -> list[str]:
|
|
54
|
+
return [COMPOSER_LOCK_FILE_NAME]
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
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('Pipenv Restore Dependencies')
|
|
12
|
+
|
|
13
|
+
PIPENV_MANIFEST_FILE_NAME = 'Pipfile'
|
|
14
|
+
PIPENV_LOCK_FILE_NAME = 'Pipfile.lock'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RestorePipenvDependencies(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 == PIPENV_MANIFEST_FILE_NAME
|
|
23
|
+
|
|
24
|
+
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
|
|
25
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
26
|
+
lockfile_path = Path(manifest_dir) / PIPENV_LOCK_FILE_NAME if manifest_dir else None
|
|
27
|
+
|
|
28
|
+
if lockfile_path and lockfile_path.is_file():
|
|
29
|
+
# Lockfile already exists — read it directly without running pipenv
|
|
30
|
+
content = get_file_content(str(lockfile_path))
|
|
31
|
+
relative_path = build_dep_tree_path(document.path, PIPENV_LOCK_FILE_NAME)
|
|
32
|
+
logger.debug('Using existing Pipfile.lock, %s', {'path': str(lockfile_path)})
|
|
33
|
+
return Document(relative_path, content, self.is_git_diff)
|
|
34
|
+
|
|
35
|
+
# Lockfile absent — generate it
|
|
36
|
+
return super().try_restore_dependencies(document)
|
|
37
|
+
|
|
38
|
+
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
|
|
39
|
+
return [['pipenv', 'lock']]
|
|
40
|
+
|
|
41
|
+
def get_lock_file_name(self) -> str:
|
|
42
|
+
return PIPENV_LOCK_FILE_NAME
|
|
43
|
+
|
|
44
|
+
def get_lock_file_names(self) -> list[str]:
|
|
45
|
+
return [PIPENV_LOCK_FILE_NAME]
|
|
@@ -0,0 +1,62 @@
|
|
|
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('Poetry Restore Dependencies')
|
|
12
|
+
|
|
13
|
+
POETRY_MANIFEST_FILE_NAME = 'pyproject.toml'
|
|
14
|
+
POETRY_LOCK_FILE_NAME = 'poetry.lock'
|
|
15
|
+
|
|
16
|
+
# Section header that signals this pyproject.toml is managed by Poetry
|
|
17
|
+
_POETRY_TOOL_SECTION = '[tool.poetry]'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _indicates_poetry(pyproject_content: Optional[str]) -> bool:
|
|
21
|
+
"""Return True if pyproject.toml content signals that this project uses Poetry."""
|
|
22
|
+
if not pyproject_content:
|
|
23
|
+
return False
|
|
24
|
+
return _POETRY_TOOL_SECTION in pyproject_content
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RestorePoetryDependencies(BaseRestoreDependencies):
|
|
28
|
+
def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) -> None:
|
|
29
|
+
super().__init__(ctx, is_git_diff, command_timeout)
|
|
30
|
+
|
|
31
|
+
def is_project(self, document: Document) -> bool:
|
|
32
|
+
if Path(document.path).name != POETRY_MANIFEST_FILE_NAME:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
36
|
+
if manifest_dir and (Path(manifest_dir) / POETRY_LOCK_FILE_NAME).is_file():
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
return _indicates_poetry(document.content)
|
|
40
|
+
|
|
41
|
+
def try_restore_dependencies(self, document: Document) -> Optional[Document]:
|
|
42
|
+
manifest_dir = self.get_manifest_dir(document)
|
|
43
|
+
lockfile_path = Path(manifest_dir) / POETRY_LOCK_FILE_NAME if manifest_dir else None
|
|
44
|
+
|
|
45
|
+
if lockfile_path and lockfile_path.is_file():
|
|
46
|
+
# Lockfile already exists — read it directly without running poetry
|
|
47
|
+
content = get_file_content(str(lockfile_path))
|
|
48
|
+
relative_path = build_dep_tree_path(document.path, POETRY_LOCK_FILE_NAME)
|
|
49
|
+
logger.debug('Using existing poetry.lock, %s', {'path': str(lockfile_path)})
|
|
50
|
+
return Document(relative_path, content, self.is_git_diff)
|
|
51
|
+
|
|
52
|
+
# Lockfile absent but Poetry is indicated in pyproject.toml — generate it
|
|
53
|
+
return super().try_restore_dependencies(document)
|
|
54
|
+
|
|
55
|
+
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
|
|
56
|
+
return [['poetry', 'lock']]
|
|
57
|
+
|
|
58
|
+
def get_lock_file_name(self) -> str:
|
|
59
|
+
return POETRY_LOCK_FILE_NAME
|
|
60
|
+
|
|
61
|
+
def get_lock_file_names(self) -> list[str]:
|
|
62
|
+
return [POETRY_LOCK_FILE_NAME]
|
|
@@ -9,8 +9,14 @@ from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestore
|
|
|
9
9
|
from cycode.cli.files_collector.sca.go.restore_go_dependencies import RestoreGoDependencies
|
|
10
10
|
from cycode.cli.files_collector.sca.maven.restore_gradle_dependencies import RestoreGradleDependencies
|
|
11
11
|
from cycode.cli.files_collector.sca.maven.restore_maven_dependencies import RestoreMavenDependencies
|
|
12
|
+
from cycode.cli.files_collector.sca.npm.restore_deno_dependencies import RestoreDenoDependencies
|
|
12
13
|
from cycode.cli.files_collector.sca.npm.restore_npm_dependencies import RestoreNpmDependencies
|
|
14
|
+
from cycode.cli.files_collector.sca.npm.restore_pnpm_dependencies import RestorePnpmDependencies
|
|
15
|
+
from cycode.cli.files_collector.sca.npm.restore_yarn_dependencies import RestoreYarnDependencies
|
|
13
16
|
from cycode.cli.files_collector.sca.nuget.restore_nuget_dependencies import RestoreNugetDependencies
|
|
17
|
+
from cycode.cli.files_collector.sca.php.restore_composer_dependencies import RestoreComposerDependencies
|
|
18
|
+
from cycode.cli.files_collector.sca.python.restore_pipenv_dependencies import RestorePipenvDependencies
|
|
19
|
+
from cycode.cli.files_collector.sca.python.restore_poetry_dependencies import RestorePoetryDependencies
|
|
14
20
|
from cycode.cli.files_collector.sca.ruby.restore_ruby_dependencies import RestoreRubyDependencies
|
|
15
21
|
from cycode.cli.files_collector.sca.sbt.restore_sbt_dependencies import RestoreSbtDependencies
|
|
16
22
|
from cycode.cli.models import Document
|
|
@@ -143,8 +149,14 @@ def _get_restore_handlers(ctx: typer.Context, is_git_diff: bool) -> list[BaseRes
|
|
|
143
149
|
RestoreSbtDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
144
150
|
RestoreGoDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
145
151
|
RestoreNugetDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
146
|
-
|
|
152
|
+
RestoreYarnDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
153
|
+
RestorePnpmDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
154
|
+
RestoreDenoDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
155
|
+
RestoreNpmDependencies(ctx, is_git_diff, build_dep_tree_timeout), # Must be after Yarn & Pnpm for fallback
|
|
147
156
|
RestoreRubyDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
157
|
+
RestorePoetryDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
158
|
+
RestorePipenvDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
159
|
+
RestoreComposerDependencies(ctx, is_git_diff, build_dep_tree_timeout),
|
|
148
160
|
]
|
|
149
161
|
|
|
150
162
|
|
|
@@ -17,7 +17,11 @@ def _validate_zip_file_size(scan_type: str, zip_file_size: int) -> None:
|
|
|
17
17
|
raise custom_exceptions.ZipTooLargeError(max_size_limit)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def zip_documents(
|
|
20
|
+
def zip_documents(
|
|
21
|
+
scan_type: str,
|
|
22
|
+
documents: list[Document],
|
|
23
|
+
zip_file: Optional[InMemoryZip] = None,
|
|
24
|
+
) -> InMemoryZip:
|
|
21
25
|
if zip_file is None:
|
|
22
26
|
zip_file = InMemoryZip()
|
|
23
27
|
|
cycode/cli/utils/scan_batch.py
CHANGED
|
@@ -111,9 +111,13 @@ def run_parallel_batched_scan(
|
|
|
111
111
|
scan_type: str,
|
|
112
112
|
documents: list[Document],
|
|
113
113
|
progress_bar: 'BaseProgressBar',
|
|
114
|
+
skip_batching: bool = False,
|
|
114
115
|
) -> tuple[dict[str, 'CliError'], list['LocalScanResult']]:
|
|
115
116
|
# batching is disabled for SCA; requested by Mor
|
|
116
|
-
|
|
117
|
+
if scan_type == consts.SCA_SCAN_TYPE or skip_batching:
|
|
118
|
+
batches = [documents]
|
|
119
|
+
else:
|
|
120
|
+
batches = split_documents_into_batches(scan_type, documents)
|
|
117
121
|
|
|
118
122
|
progress_bar.set_section_length(ScanProgressBarSection.SCAN, len(batches)) # * 3
|
|
119
123
|
# TODO(MarshalX): we should multiply the count of batches in SCAN section because each batch has 3 steps:
|
cycode/cli/utils/scan_utils.py
CHANGED
|
@@ -5,6 +5,7 @@ from uuid import UUID, uuid4
|
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
|
+
from cycode.cli import consts
|
|
8
9
|
from cycode.cli.cli_types import SeverityOption
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -31,6 +32,10 @@ def is_cycodeignore_allowed_by_scan_config(ctx: typer.Context) -> bool:
|
|
|
31
32
|
return scan_config.is_cycode_ignore_allowed if scan_config else True
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def should_use_presigned_upload(scan_type: str) -> bool:
|
|
36
|
+
return scan_type in consts.PRESIGNED_UPLOAD_SCAN_TYPES
|
|
37
|
+
|
|
38
|
+
|
|
34
39
|
def generate_unique_scan_id() -> UUID:
|
|
35
40
|
if 'PYTEST_TEST_UNIQUE_ID' in os.environ:
|
|
36
41
|
return UUID(os.environ['PYTEST_TEST_UNIQUE_ID'])
|
cycode/cyclient/models.py
CHANGED
|
@@ -114,6 +114,26 @@ class ScanResultSchema(Schema):
|
|
|
114
114
|
return ScanResult(**data)
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
@dataclass
|
|
118
|
+
class UploadLinkResponse:
|
|
119
|
+
upload_id: str
|
|
120
|
+
url: str
|
|
121
|
+
presigned_post_fields: dict[str, str]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class UploadLinkResponseSchema(Schema):
|
|
125
|
+
class Meta:
|
|
126
|
+
unknown = EXCLUDE
|
|
127
|
+
|
|
128
|
+
upload_id = fields.String()
|
|
129
|
+
url = fields.String()
|
|
130
|
+
presigned_post_fields = fields.Dict(keys=fields.String(), values=fields.String())
|
|
131
|
+
|
|
132
|
+
@post_load
|
|
133
|
+
def build_dto(self, data: dict[str, Any], **_) -> 'UploadLinkResponse':
|
|
134
|
+
return UploadLinkResponse(**data)
|
|
135
|
+
|
|
136
|
+
|
|
117
137
|
class ScanInitializationResponse(Schema):
|
|
118
138
|
def __init__(self, scan_id: Optional[str] = None, err: Optional[str] = None) -> None:
|
|
119
139
|
super().__init__()
|