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.
Files changed (30) hide show
  1. cycode/__init__.py +1 -1
  2. cycode/cli/apps/report/sbom/path/path_command.py +11 -14
  3. cycode/cli/apps/sca_options.py +47 -0
  4. cycode/cli/apps/scan/code_scanner.py +76 -4
  5. cycode/cli/apps/scan/commit_range_scanner.py +51 -8
  6. cycode/cli/apps/scan/scan_command.py +10 -30
  7. cycode/cli/cli_types.py +1 -0
  8. cycode/cli/consts.py +5 -2
  9. cycode/cli/files_collector/sca/base_restore_dependencies.py +28 -4
  10. cycode/cli/files_collector/sca/go/restore_go_dependencies.py +4 -4
  11. cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +46 -0
  12. cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +23 -136
  13. cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +70 -0
  14. cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +70 -0
  15. cycode/cli/files_collector/sca/php/__init__.py +0 -0
  16. cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +54 -0
  17. cycode/cli/files_collector/sca/python/__init__.py +0 -0
  18. cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +45 -0
  19. cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +62 -0
  20. cycode/cli/files_collector/sca/sca_file_collector.py +13 -1
  21. cycode/cli/files_collector/zip_documents.py +5 -1
  22. cycode/cli/utils/scan_batch.py +5 -1
  23. cycode/cli/utils/scan_utils.py +5 -0
  24. cycode/cyclient/models.py +20 -0
  25. cycode/cyclient/scan_client.py +61 -0
  26. {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/METADATA +31 -11
  27. {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/RECORD +30 -21
  28. {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/WHEEL +0 -0
  29. {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/entry_points.txt +0 -0
  30. {cycode-3.10.3.dev1.dist-info → cycode-3.10.4.dev1.dist-info}/licenses/LICENCE +0 -0
@@ -1,21 +1,17 @@
1
- import os
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, build_dep_tree_path
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
- return any(document.path.endswith(ext) for ext in NPM_PROJECT_FILE_EXTENSIONS)
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
- return manifest_dir
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
- lock_file_paths = [os.path.join(manifest_dir, lock_file_name) for lock_file_name in NPM_LOCK_FILE_NAMES]
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
- return existing_lock_file, checked_lockfiles
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
- def _create_document_from_lockfile(self, document: Document, lockfile_path: str) -> Optional[Document]:
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 NPM_LOCK_FILE_NAMES
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
- # Use os.path.dirname to handle both Unix (/) and Windows (\) separators
176
- # This is cross-platform and handles edge cases correctly
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
- RestoreNpmDependencies(ctx, is_git_diff, build_dep_tree_timeout),
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(scan_type: str, documents: list[Document], zip_file: Optional[InMemoryZip] = None) -> InMemoryZip:
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
 
@@ -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
- batches = [documents] if scan_type == consts.SCA_SCAN_TYPE else split_documents_into_batches(scan_type, documents)
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:
@@ -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__()