cycode 3.25.1.dev2__py3-none-any.whl → 3.25.1.dev4__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 CHANGED
@@ -5,4 +5,4 @@ import time as _time
5
5
  # end-to-end scan duration from the moment the user actually triggered it.
6
6
  _BOOT_WALL: float = _time.time()
7
7
 
8
- __version__ = '3.25.1.dev2' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
8
+ __version__ = '3.25.1.dev4' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
@@ -6,15 +6,19 @@ from typing import Optional
6
6
  import typer
7
7
 
8
8
  from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path
9
+ from cycode.cli.files_collector.sca.npm.workspace import (
10
+ BUN_LOCK_FILE_NAME,
11
+ MANIFEST_FILE_NAME,
12
+ is_covered_workspace_member,
13
+ )
9
14
  from cycode.cli.models import Document
10
- from cycode.cli.utils.path_utils import get_file_content
15
+ from cycode.cli.utils.path_utils import get_file_content, get_scan_roots_from_context
11
16
  from cycode.cli.utils.shell_executor import shell
12
17
  from cycode.logger import get_logger
13
18
 
14
19
  logger = get_logger('Bun Restore Dependencies')
15
20
 
16
- BUN_MANIFEST_FILE_NAME = 'package.json'
17
- BUN_LOCK_FILE_NAME = 'bun.lock'
21
+ BUN_MANIFEST_FILE_NAME = MANIFEST_FILE_NAME
18
22
 
19
23
  # Only Bun >=1.2 produces the text-based `bun.lock` lockfile that we parse.
20
24
  # Older Bun versions emit a binary `bun.lockb`, which is not supported.
@@ -61,6 +65,9 @@ class RestoreBunDependencies(BaseRestoreDependencies):
61
65
  if manifest_dir and (Path(manifest_dir) / BUN_LOCK_FILE_NAME).is_file():
62
66
  return True
63
67
 
68
+ if is_covered_workspace_member(manifest_dir, document.path, get_scan_roots_from_context(self.ctx)):
69
+ return False
70
+
64
71
  return _indicates_bun(document.content)
65
72
 
66
73
  def _is_supported_bun_version(self) -> bool:
@@ -4,6 +4,7 @@ from typing import Optional
4
4
  import typer
5
5
 
6
6
  from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path
7
+ from cycode.cli.files_collector.sca.npm.workspace import DENO_LOCK_FILE_NAME
7
8
  from cycode.cli.models import Document
8
9
  from cycode.cli.utils.path_utils import get_file_content
9
10
  from cycode.logger import get_logger
@@ -11,7 +12,6 @@ from cycode.logger import get_logger
11
12
  logger = get_logger('Deno Restore Dependencies')
12
13
 
13
14
  DENO_MANIFEST_FILE_NAMES = ('deno.json', 'deno.jsonc')
14
- DENO_LOCK_FILE_NAME = 'deno.lock'
15
15
 
16
16
 
17
17
  class RestoreDenoDependencies(BaseRestoreDependencies):
@@ -3,15 +3,25 @@ from pathlib import Path
3
3
  import typer
4
4
 
5
5
  from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies
6
+ from cycode.cli.files_collector.sca.npm.workspace import (
7
+ BUN_LOCK_FILE_NAME,
8
+ DENO_LOCK_FILE_NAME,
9
+ MANIFEST_FILE_NAME,
10
+ NPM_LOCK_FILE_NAME,
11
+ NPM_SHRINKWRAP_FILE_NAME,
12
+ PNPM_LOCK_FILE_NAME,
13
+ YARN_LOCK_FILE_NAME,
14
+ is_covered_workspace_member,
15
+ )
6
16
  from cycode.cli.models import Document
17
+ from cycode.cli.utils.path_utils import get_scan_roots_from_context
7
18
  from cycode.logger import get_logger
8
19
 
9
20
  logger = get_logger('NPM Restore Dependencies')
10
21
 
11
- NPM_MANIFEST_FILE_NAME = 'package.json'
12
- NPM_LOCK_FILE_NAME = 'package-lock.json'
22
+ NPM_MANIFEST_FILE_NAME = MANIFEST_FILE_NAME
13
23
  # These lockfiles indicate another package manager owns the project — NPM should not run
14
- _ALTERNATIVE_LOCK_FILES = ('yarn.lock', 'pnpm-lock.yaml', 'deno.lock', 'bun.lock')
24
+ _ALTERNATIVE_LOCK_FILES = (YARN_LOCK_FILE_NAME, PNPM_LOCK_FILE_NAME, DENO_LOCK_FILE_NAME, BUN_LOCK_FILE_NAME)
15
25
 
16
26
 
17
27
  class RestoreNpmDependencies(BaseRestoreDependencies):
@@ -24,6 +34,9 @@ class RestoreNpmDependencies(BaseRestoreDependencies):
24
34
  Yarn and pnpm projects are handled by their dedicated handlers, which run before
25
35
  this one in the handler list. This handler is the npm fallback.
26
36
 
37
+ A manifest is also declined when a lockfile further up already resolves it, whichever
38
+ package manager wrote that lockfile; see the workspace package for how that is decided.
39
+
27
40
  NOTE: this guard only excludes a project when an alternative lockfile is *physically
28
41
  present on disk*. It does not inspect the `packageManager`/`engines` signal in
29
42
  package.json. So a project that declares e.g. `packageManager: "bun@..."` (or pnpm)
@@ -37,16 +50,18 @@ class RestoreNpmDependencies(BaseRestoreDependencies):
37
50
  return False
38
51
 
39
52
  manifest_dir = self.get_manifest_dir(document)
40
- if manifest_dir:
41
- for lock_file in _ALTERNATIVE_LOCK_FILES:
42
- if (Path(manifest_dir) / lock_file).is_file():
43
- logger.debug(
44
- 'Skipping npm restore: alternative lockfile detected, %s',
45
- {'path': document.path, 'lockfile': lock_file},
46
- )
47
- return False
53
+ if not manifest_dir:
54
+ return True
55
+
56
+ for lock_file in _ALTERNATIVE_LOCK_FILES:
57
+ if (Path(manifest_dir) / lock_file).is_file():
58
+ logger.debug(
59
+ 'Skipping npm restore: alternative lockfile detected, %s',
60
+ {'path': document.path, 'lockfile': lock_file},
61
+ )
62
+ return False
48
63
 
49
- return True
64
+ return not is_covered_workspace_member(manifest_dir, document.path, get_scan_roots_from_context(self.ctx))
50
65
 
51
66
  def get_commands(self, manifest_file_path: str) -> list[list[str]]:
52
67
  return [
@@ -65,7 +80,11 @@ class RestoreNpmDependencies(BaseRestoreDependencies):
65
80
  return NPM_LOCK_FILE_NAME
66
81
 
67
82
  def get_lock_file_names(self) -> list[str]:
68
- return [NPM_LOCK_FILE_NAME]
83
+ return [NPM_LOCK_FILE_NAME, NPM_SHRINKWRAP_FILE_NAME]
84
+
85
+ def get_restored_lock_file_name(self, restore_file_path: str) -> str:
86
+ name = Path(restore_file_path).name
87
+ return name if name in self.get_lock_file_names() else self.get_lock_file_name()
69
88
 
70
89
  @staticmethod
71
90
  def prepare_manifest_file_path_for_command(manifest_file_path: str) -> str:
@@ -5,14 +5,18 @@ from typing import Optional
5
5
  import typer
6
6
 
7
7
  from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path
8
+ from cycode.cli.files_collector.sca.npm.workspace import (
9
+ MANIFEST_FILE_NAME,
10
+ PNPM_LOCK_FILE_NAME,
11
+ is_covered_workspace_member,
12
+ )
8
13
  from cycode.cli.models import Document
9
- from cycode.cli.utils.path_utils import get_file_content
14
+ from cycode.cli.utils.path_utils import get_file_content, get_scan_roots_from_context
10
15
  from cycode.logger import get_logger
11
16
 
12
17
  logger = get_logger('Pnpm Restore Dependencies')
13
18
 
14
- PNPM_MANIFEST_FILE_NAME = 'package.json'
15
- PNPM_LOCK_FILE_NAME = 'pnpm-lock.yaml'
19
+ PNPM_MANIFEST_FILE_NAME = MANIFEST_FILE_NAME
16
20
 
17
21
 
18
22
  def _indicates_pnpm(package_json_content: Optional[str]) -> bool:
@@ -44,6 +48,9 @@ class RestorePnpmDependencies(BaseRestoreDependencies):
44
48
  if manifest_dir and (Path(manifest_dir) / PNPM_LOCK_FILE_NAME).is_file():
45
49
  return True
46
50
 
51
+ if is_covered_workspace_member(manifest_dir, document.path, get_scan_roots_from_context(self.ctx)):
52
+ return False
53
+
47
54
  return _indicates_pnpm(document.content)
48
55
 
49
56
  def try_restore_dependencies(self, document: Document) -> Optional[Document]:
@@ -5,14 +5,18 @@ from typing import Optional
5
5
  import typer
6
6
 
7
7
  from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path
8
+ from cycode.cli.files_collector.sca.npm.workspace import (
9
+ MANIFEST_FILE_NAME,
10
+ YARN_LOCK_FILE_NAME,
11
+ is_covered_workspace_member,
12
+ )
8
13
  from cycode.cli.models import Document
9
- from cycode.cli.utils.path_utils import get_file_content
14
+ from cycode.cli.utils.path_utils import get_file_content, get_scan_roots_from_context
10
15
  from cycode.logger import get_logger
11
16
 
12
17
  logger = get_logger('Yarn Restore Dependencies')
13
18
 
14
- YARN_MANIFEST_FILE_NAME = 'package.json'
15
- YARN_LOCK_FILE_NAME = 'yarn.lock'
19
+ YARN_MANIFEST_FILE_NAME = MANIFEST_FILE_NAME
16
20
 
17
21
 
18
22
  def _indicates_yarn(package_json_content: Optional[str]) -> bool:
@@ -44,6 +48,9 @@ class RestoreYarnDependencies(BaseRestoreDependencies):
44
48
  if manifest_dir and (Path(manifest_dir) / YARN_LOCK_FILE_NAME).is_file():
45
49
  return True
46
50
 
51
+ if is_covered_workspace_member(manifest_dir, document.path, get_scan_roots_from_context(self.ctx)):
52
+ return False
53
+
47
54
  return _indicates_yarn(document.content)
48
55
 
49
56
  def try_restore_dependencies(self, document: Document) -> Optional[Document]:
@@ -0,0 +1,49 @@
1
+ """Deciding whether a lockfile above a manifest already resolves that manifest's dependencies.
2
+
3
+ The lockfile is the authority wherever it can name its members; the manifest's workspaces globs
4
+ are a fallback for the formats that cannot. See resolvers.py for the per-format readers.
5
+ """
6
+
7
+ from cycode.cli.files_collector.sca.npm.workspace import coverage as _coverage
8
+ from cycode.cli.files_collector.sca.npm.workspace import globs as _globs
9
+ from cycode.cli.files_collector.sca.npm.workspace import resolvers as _resolvers
10
+ from cycode.cli.files_collector.sca.npm.workspace.coverage import (
11
+ WorkspaceCoverage,
12
+ find_covering_workspace,
13
+ is_covered_workspace_member,
14
+ )
15
+ from cycode.cli.files_collector.sca.npm.workspace.names import (
16
+ BUN_BINARY_LOCK_FILE_NAME,
17
+ BUN_LOCK_FILE_NAME,
18
+ DENO_LOCK_FILE_NAME,
19
+ MANIFEST_FILE_NAME,
20
+ NPM_LOCK_FILE_NAME,
21
+ NPM_SHRINKWRAP_FILE_NAME,
22
+ PNPM_LOCK_FILE_NAME,
23
+ YARN_LOCK_FILE_NAME,
24
+ )
25
+ from cycode.cli.files_collector.sca.npm.workspace.resolvers import MEMBER_RESOLVERS
26
+
27
+
28
+ def clear_cache() -> None:
29
+ """Drop every memo, so one scan never inherits another scan's view of the filesystem."""
30
+ _resolvers.clear_cache()
31
+ _globs.clear_cache()
32
+ _coverage.clear_cache()
33
+
34
+
35
+ __all__ = [
36
+ 'BUN_BINARY_LOCK_FILE_NAME',
37
+ 'BUN_LOCK_FILE_NAME',
38
+ 'DENO_LOCK_FILE_NAME',
39
+ 'MANIFEST_FILE_NAME',
40
+ 'MEMBER_RESOLVERS',
41
+ 'NPM_LOCK_FILE_NAME',
42
+ 'NPM_SHRINKWRAP_FILE_NAME',
43
+ 'PNPM_LOCK_FILE_NAME',
44
+ 'YARN_LOCK_FILE_NAME',
45
+ 'WorkspaceCoverage',
46
+ 'clear_cache',
47
+ 'find_covering_workspace',
48
+ 'is_covered_workspace_member',
49
+ ]
@@ -0,0 +1,142 @@
1
+ """Walking up from a manifest to the lockfile that already resolves it."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, NamedTuple, Optional
6
+
7
+ from cycode.cli.files_collector.sca.npm.workspace.files import logger, resolved_path
8
+ from cycode.cli.files_collector.sca.npm.workspace.globs import declares_workspace_member
9
+ from cycode.cli.files_collector.sca.npm.workspace.resolvers import MEMBER_RESOLVERS
10
+ from cycode.cli.utils.path_utils import is_sub_path
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Iterator
14
+
15
+ _GIT_DIR_NAME = '.git'
16
+
17
+ _reported_unscanned_roots: set = set()
18
+
19
+
20
+ def clear_cache() -> None:
21
+ _reported_unscanned_roots.clear()
22
+
23
+
24
+ class WorkspaceCoverage(NamedTuple):
25
+ package_manager: str
26
+ lock_file: Path
27
+
28
+
29
+ def _scan_root_directories(scan_roots: tuple) -> list:
30
+ """The directory each scanned path stands for; scanning a file scans its directory."""
31
+ directories = []
32
+ for scan_root in scan_roots:
33
+ resolved = resolved_path(scan_root)
34
+ if os.path.isfile(resolved):
35
+ resolved = os.path.dirname(resolved)
36
+
37
+ if resolved:
38
+ directories.append(resolved)
39
+
40
+ return directories
41
+
42
+
43
+ def _containing_scan_roots(manifest_dir: Path, scan_roots: tuple) -> list:
44
+ resolved_manifest_dir = resolved_path(manifest_dir)
45
+ return [
46
+ Path(directory)
47
+ for directory in _scan_root_directories(scan_roots)
48
+ if is_sub_path(directory, resolved_manifest_dir)
49
+ ]
50
+
51
+
52
+ def _resolve_walk_boundary(manifest_dir: Path, scan_roots: tuple) -> Optional[Path]:
53
+ for root_dir in manifest_dir.parents:
54
+ if (root_dir / _GIT_DIR_NAME).exists():
55
+ return root_dir
56
+
57
+ containing = _containing_scan_roots(manifest_dir, scan_roots)
58
+ if containing:
59
+ return min(containing, key=lambda scan_root: len(scan_root.parts))
60
+
61
+ if scan_roots:
62
+ return manifest_dir
63
+
64
+ return None
65
+
66
+
67
+ def _workspace_root_candidates(manifest_dir: Path, scan_roots: tuple) -> 'Iterator[Path]':
68
+ boundary = _resolve_walk_boundary(manifest_dir, scan_roots)
69
+ resolved_boundary = resolved_path(boundary) if boundary is not None else None
70
+ if resolved_boundary == resolved_path(manifest_dir):
71
+ return
72
+
73
+ for root_dir in manifest_dir.parents:
74
+ yield root_dir
75
+ if resolved_boundary is not None and resolved_path(root_dir) == resolved_boundary:
76
+ return
77
+
78
+
79
+ def _find_covering_workspace(manifest_dir: Path, scan_roots: tuple) -> Optional[WorkspaceCoverage]:
80
+ for root_dir in _workspace_root_candidates(manifest_dir, scan_roots):
81
+ member_path = manifest_dir.relative_to(root_dir).as_posix()
82
+
83
+ for resolver in MEMBER_RESOLVERS:
84
+ for lock_file_name in resolver.lock_file_names:
85
+ lock_file = root_dir / lock_file_name
86
+ if not lock_file.is_file():
87
+ continue
88
+
89
+ member_names = resolver.resolve(lock_file)
90
+ if member_names is not None:
91
+ if member_path in member_names:
92
+ return WorkspaceCoverage(resolver.package_manager, lock_file)
93
+ continue
94
+
95
+ if resolver.may_use_workspace_globs and declares_workspace_member(root_dir, member_path):
96
+ return WorkspaceCoverage(resolver.package_manager, lock_file)
97
+
98
+ return None
99
+
100
+
101
+ def find_covering_workspace(manifest_dir: Optional[str], scan_roots: tuple = ()) -> Optional[WorkspaceCoverage]:
102
+ if not manifest_dir:
103
+ return None
104
+
105
+ return _find_covering_workspace(Path(manifest_dir), scan_roots)
106
+
107
+
108
+ def _is_inside_scanned_paths(scan_roots: tuple, root_dir: Path) -> bool:
109
+ directories = _scan_root_directories(scan_roots)
110
+ if not directories:
111
+ logger.debug('No scanned paths in context; treating the workspace root as scanned, %s', {'root': str(root_dir)})
112
+ return True
113
+
114
+ resolved_root_dir = resolved_path(root_dir)
115
+ return any(is_sub_path(directory, resolved_root_dir) for directory in directories)
116
+
117
+
118
+ def is_covered_workspace_member(manifest_dir: Optional[str], document_path: str, scan_roots: tuple = ()) -> bool:
119
+ coverage = find_covering_workspace(manifest_dir, scan_roots)
120
+ if coverage is None:
121
+ return False
122
+
123
+ details = {
124
+ 'path': document_path,
125
+ 'root_lockfile': str(coverage.lock_file),
126
+ 'workspace': coverage.package_manager,
127
+ }
128
+ if _is_inside_scanned_paths(scan_roots, coverage.lock_file.parent):
129
+ logger.debug('Skipping restore: the workspace root lockfile already covers this member, %s', details)
130
+ return True
131
+
132
+ report_key = (document_path, str(coverage.lock_file))
133
+ if report_key not in _reported_unscanned_roots:
134
+ _reported_unscanned_roots.add(report_key)
135
+ logger.warning(
136
+ 'The workspace root lockfile is outside the scanned path and will not be collected, '
137
+ 'so this member is restored on its own. Scan the workspace root for the versions it '
138
+ 'actually installs, %s',
139
+ details,
140
+ )
141
+
142
+ return False
@@ -0,0 +1,38 @@
1
+ """Filesystem access shared by the readers: identity for caching, and tolerant parsing."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from cycode.cli.utils.path_utils import get_absolute_path
9
+ from cycode.logger import get_logger
10
+
11
+ logger = get_logger('SCA NPM Workspace')
12
+
13
+ FileStamp = tuple[str, int, int]
14
+
15
+
16
+ def resolved_path(path: object) -> str:
17
+ return os.path.realpath(get_absolute_path(str(path)))
18
+
19
+
20
+ def file_stamp(path: Path) -> Optional[FileStamp]:
21
+ try:
22
+ stat_result = path.stat()
23
+ except OSError:
24
+ return None
25
+
26
+ return str(path), stat_result.st_mtime_ns, stat_result.st_size
27
+
28
+
29
+ def read_json_object(path: Path) -> Optional[dict]:
30
+ try:
31
+ content = json.loads(path.read_text(encoding='UTF-8'))
32
+ except FileNotFoundError:
33
+ return None
34
+ except (OSError, ValueError) as e:
35
+ logger.debug('Could not read an npm workspace file, %s', {'path': str(path), 'error': e})
36
+ return None
37
+
38
+ return content if isinstance(content, dict) else None
@@ -0,0 +1,116 @@
1
+ """Matching a member against the manifest's workspaces globs.
2
+
3
+ Only reached for lockfiles that cannot name their own members: classic yarn.lock and the
4
+ binary bun.lockb. Every other format answers from the lockfile itself.
5
+ """
6
+
7
+ import fnmatch
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from typing import NamedTuple
11
+
12
+ from cycode.cli.files_collector.sca.npm.workspace.files import FileStamp, file_stamp, read_json_object
13
+ from cycode.cli.files_collector.sca.npm.workspace.names import MANIFEST_FILE_NAME
14
+
15
+ _MANIFEST_WORKSPACES_SECTION = 'workspaces'
16
+ _MANIFEST_WORKSPACE_PACKAGES_SECTION = 'packages'
17
+ _NEGATION_PREFIX = '!'
18
+ _GLOBSTAR = '**'
19
+ _SEGMENT_MATCH_CACHE_SIZE = 4096
20
+
21
+
22
+ class _WorkspacePatterns(NamedTuple):
23
+ included: tuple[str, ...]
24
+ excluded: tuple[str, ...]
25
+
26
+
27
+ _EMPTY_WORKSPACE_PATTERNS = _WorkspacePatterns((), ())
28
+
29
+ _workspace_patterns_cache: dict[FileStamp, _WorkspacePatterns] = {}
30
+
31
+
32
+ def clear_cache() -> None:
33
+ _workspace_patterns_cache.clear()
34
+
35
+
36
+ def _matches_workspace_pattern(pattern: str, member_path: str) -> bool:
37
+ """Match a workspace glob against a member path, one path segment at a time.
38
+
39
+ fnmatch is wrong for a whole path because its * crosses a separator, but inside one
40
+ segment there is no separator, so it is exactly right there - and it brings character
41
+ classes with it. Only ** needs handling here, because only ** spans segments.
42
+ """
43
+ return _match_segments(tuple(pattern.split('/')), tuple(member_path.split('/')))
44
+
45
+
46
+ @lru_cache(maxsize=_SEGMENT_MATCH_CACHE_SIZE)
47
+ def _match_segments(pattern_segments: tuple, path_segments: tuple) -> bool:
48
+ """Memoised so that several ** in one pattern cannot make this exponential.
49
+
50
+ Each ** tries every split of the remaining path, so without memoisation a pattern such as
51
+ **/**/**/x against a deep tree multiplies out. The arguments are plain tuples of strings,
52
+ so a result can never go stale.
53
+ """
54
+ if not pattern_segments:
55
+ return not path_segments
56
+
57
+ head, rest = pattern_segments[0], pattern_segments[1:]
58
+ if head == _GLOBSTAR:
59
+ # ** spans zero or more segments, and spanning zero is what makes dir/** match dir
60
+ return any(_match_segments(rest, path_segments[index:]) for index in range(len(path_segments) + 1))
61
+
62
+ if not path_segments:
63
+ return False
64
+
65
+ return fnmatch.fnmatchcase(path_segments[0], head) and _match_segments(rest, path_segments[1:])
66
+
67
+
68
+ def _split_workspace_patterns(declared: list) -> _WorkspacePatterns:
69
+ included = []
70
+ excluded = []
71
+ for entry in declared:
72
+ stripped = entry.strip()
73
+ is_excluded = stripped.startswith(_NEGATION_PREFIX)
74
+ normalized = (stripped[1:] if is_excluded else stripped).strip()
75
+ if normalized.startswith('./'):
76
+ normalized = normalized[2:]
77
+
78
+ normalized = normalized.rstrip('/')
79
+ if not normalized:
80
+ continue
81
+
82
+ if is_excluded:
83
+ excluded.append(normalized)
84
+ else:
85
+ included.append(normalized)
86
+
87
+ return _WorkspacePatterns(tuple(included), tuple(excluded))
88
+
89
+
90
+ def _read_manifest_workspace_patterns(root_dir: Path) -> _WorkspacePatterns:
91
+ manifest = root_dir / MANIFEST_FILE_NAME
92
+ stamp = file_stamp(manifest)
93
+ if stamp is None:
94
+ return _EMPTY_WORKSPACE_PATTERNS
95
+
96
+ cached = _workspace_patterns_cache.get(stamp)
97
+ if cached is not None:
98
+ return cached
99
+
100
+ content = read_json_object(manifest)
101
+ workspaces = content.get(_MANIFEST_WORKSPACES_SECTION) if content is not None else None
102
+ if isinstance(workspaces, dict):
103
+ workspaces = workspaces.get(_MANIFEST_WORKSPACE_PACKAGES_SECTION)
104
+
105
+ declared = [entry for entry in workspaces if isinstance(entry, str)] if isinstance(workspaces, list) else []
106
+ patterns = _split_workspace_patterns(declared)
107
+ _workspace_patterns_cache[stamp] = patterns
108
+ return patterns
109
+
110
+
111
+ def declares_workspace_member(root_dir: Path, member_path: str) -> bool:
112
+ patterns = _read_manifest_workspace_patterns(root_dir)
113
+ if not any(_matches_workspace_pattern(pattern, member_path) for pattern in patterns.included):
114
+ return False
115
+
116
+ return not any(_matches_workspace_pattern(pattern, member_path) for pattern in patterns.excluded)
@@ -0,0 +1,16 @@
1
+ """File names and package-manager identifiers shared across the workspace package."""
2
+
3
+ MANIFEST_FILE_NAME = 'package.json'
4
+
5
+ NPM_PACKAGE_MANAGER = 'npm'
6
+ YARN_PACKAGE_MANAGER = 'yarn'
7
+ PNPM_PACKAGE_MANAGER = 'pnpm'
8
+ BUN_PACKAGE_MANAGER = 'bun'
9
+
10
+ NPM_LOCK_FILE_NAME = 'package-lock.json'
11
+ NPM_SHRINKWRAP_FILE_NAME = 'npm-shrinkwrap.json'
12
+ YARN_LOCK_FILE_NAME = 'yarn.lock'
13
+ PNPM_LOCK_FILE_NAME = 'pnpm-lock.yaml'
14
+ BUN_LOCK_FILE_NAME = 'bun.lock'
15
+ BUN_BINARY_LOCK_FILE_NAME = 'bun.lockb'
16
+ DENO_LOCK_FILE_NAME = 'deno.lock'
@@ -0,0 +1,294 @@
1
+ """Reading the members a root lockfile resolves, one reader per lockfile format.
2
+
3
+ A reader that returns a set is the authority on that lockfile, including when the set is empty.
4
+ None means the format cannot name its members at all, and only then does may_use_workspace_globs
5
+ decide whether the manifest globs get a say.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ from abc import ABC, abstractmethod
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import yaml
15
+
16
+ from cycode.cli.files_collector.sca.npm.workspace.files import FileStamp, file_stamp, logger, read_json_object
17
+ from cycode.cli.files_collector.sca.npm.workspace.names import (
18
+ BUN_BINARY_LOCK_FILE_NAME,
19
+ BUN_LOCK_FILE_NAME,
20
+ BUN_PACKAGE_MANAGER,
21
+ NPM_LOCK_FILE_NAME,
22
+ NPM_PACKAGE_MANAGER,
23
+ NPM_SHRINKWRAP_FILE_NAME,
24
+ PNPM_LOCK_FILE_NAME,
25
+ PNPM_PACKAGE_MANAGER,
26
+ YARN_LOCK_FILE_NAME,
27
+ YARN_PACKAGE_MANAGER,
28
+ )
29
+
30
+ _LOCKFILE_PACKAGES_SECTION = 'packages'
31
+ _NODE_MODULES_SEPARATOR = 'node_modules/'
32
+ _PNPM_LOCKFILE_IMPORTERS_SECTION = 'importers'
33
+ _PNPM_LOCKFILE_ROOT_IMPORTER = '.'
34
+ _YAML_COMMENT_PREFIX = '#'
35
+ _YARN_BERRY_MARKER = '__metadata'
36
+ _YARN_RESOLUTION_PREFIX = 'resolution:'
37
+ _YARN_WORKSPACE_PROTOCOL = re.compile(r'@workspace:([^"\',\s]+)')
38
+ _BUN_LOCKFILE_WORKSPACES_SECTION = 'workspaces'
39
+ _TRAILING_COMMA = re.compile(r',(\s*[}\]])')
40
+
41
+ _member_names_cache: dict[FileStamp, Optional[frozenset[str]]] = {}
42
+
43
+
44
+ def clear_cache() -> None:
45
+ _member_names_cache.clear()
46
+
47
+
48
+ def _normalize_member_path(member_path: str) -> Optional[str]:
49
+ normalized = member_path.strip()
50
+ if normalized.startswith('./'):
51
+ normalized = normalized[2:]
52
+
53
+ normalized = normalized.rstrip('/')
54
+ if not normalized or normalized == _PNPM_LOCKFILE_ROOT_IMPORTER:
55
+ return None
56
+
57
+ return normalized
58
+
59
+
60
+ def _npm_lockfile_member_names(lock_file: Path) -> Optional[frozenset]:
61
+ content = read_json_object(lock_file)
62
+ packages = content.get(_LOCKFILE_PACKAGES_SECTION) if content is not None else None
63
+ if not isinstance(packages, dict):
64
+ return None
65
+
66
+ return frozenset(name for name in packages if name and _NODE_MODULES_SEPARATOR not in name)
67
+
68
+
69
+ def _read_pnpm_importers_section(lock_file: Path) -> str:
70
+ """Slice out the top-level importers block so a large lockfile is not parsed in full."""
71
+ section = []
72
+ inside = False
73
+ try:
74
+ with lock_file.open(encoding='UTF-8') as lock_file_lines:
75
+ for raw_line in lock_file_lines:
76
+ line = raw_line.rstrip('\n').rstrip('\r')
77
+ if not inside:
78
+ if line.startswith(f'{_PNPM_LOCKFILE_IMPORTERS_SECTION}:'):
79
+ inside = True
80
+ section.append(line)
81
+ continue
82
+
83
+ if line.startswith(_YAML_COMMENT_PREFIX):
84
+ continue
85
+
86
+ if line and not line[0].isspace():
87
+ break
88
+
89
+ section.append(line)
90
+ except FileNotFoundError:
91
+ return ''
92
+ except (OSError, ValueError) as e:
93
+ logger.debug('Could not read a pnpm lockfile, %s', {'path': str(lock_file), 'error': e})
94
+ return ''
95
+
96
+ return '\n'.join(section)
97
+
98
+
99
+ def _pnpm_lockfile_member_names(lock_file: Path) -> Optional[frozenset]:
100
+ section = _read_pnpm_importers_section(lock_file)
101
+ if not section:
102
+ return None
103
+
104
+ try:
105
+ content = yaml.safe_load(section)
106
+ except (ValueError, yaml.YAMLError) as e:
107
+ logger.debug('Could not read a pnpm lockfile, %s', {'path': str(lock_file), 'error': e})
108
+ return None
109
+
110
+ importers = content.get(_PNPM_LOCKFILE_IMPORTERS_SECTION) if isinstance(content, dict) else None
111
+ if not isinstance(importers, dict):
112
+ return None
113
+
114
+ member_names = set()
115
+ for name in importers:
116
+ if not isinstance(name, str):
117
+ continue
118
+
119
+ normalized = _normalize_member_path(name)
120
+ if normalized:
121
+ member_names.add(normalized)
122
+
123
+ # a lockfile whose only importer is the root describes a single package, not a workspace
124
+ return frozenset(member_names) or None
125
+
126
+
127
+ def _bun_lockfile_member_names(lock_file: Path) -> Optional[frozenset]:
128
+ """bun.lock is JSON with trailing commas, and names its members under "workspaces"."""
129
+ try:
130
+ text = lock_file.read_text(encoding='UTF-8')
131
+ except FileNotFoundError:
132
+ return None
133
+ except (OSError, ValueError) as e:
134
+ logger.debug('Could not read a bun lockfile, %s', {'path': str(lock_file), 'error': e})
135
+ return None
136
+
137
+ try:
138
+ content = json.loads(text)
139
+ except ValueError:
140
+ try:
141
+ content = json.loads(_TRAILING_COMMA.sub(r'\1', text))
142
+ except ValueError as e:
143
+ logger.debug('Could not read a bun lockfile, %s', {'path': str(lock_file), 'error': e})
144
+ return None
145
+
146
+ workspaces = content.get(_BUN_LOCKFILE_WORKSPACES_SECTION) if isinstance(content, dict) else None
147
+ if not isinstance(workspaces, dict):
148
+ return None
149
+
150
+ member_names = {normalized for normalized in map(_normalize_member_path, workspaces) if normalized}
151
+ return frozenset(member_names)
152
+
153
+
154
+ def _yarn_lockfile_member_names(lock_file: Path) -> Optional[frozenset]:
155
+ """Yarn berry records every member as "<name>@workspace:<path>"; classic yarn records nothing."""
156
+ try:
157
+ text = lock_file.read_text(encoding='UTF-8', errors='replace')
158
+ except FileNotFoundError:
159
+ return None
160
+ except (OSError, ValueError) as e:
161
+ logger.debug('Could not read a yarn lockfile, %s', {'path': str(lock_file), 'error': e})
162
+ return None
163
+
164
+ if _YARN_BERRY_MARKER not in text:
165
+ return None # classic: flat, so the manifest globs are the only remaining source
166
+
167
+ member_names = set()
168
+ for line in text.splitlines():
169
+ # every entry carries a resolution naming its real path; the dependency entries carry
170
+ # ranges instead (workspace:^, workspace:*), which are not paths
171
+ if not line.strip().startswith(_YARN_RESOLUTION_PREFIX):
172
+ continue
173
+
174
+ for declared_path in _YARN_WORKSPACE_PROTOCOL.findall(line):
175
+ normalized = _normalize_member_path(declared_path)
176
+ if normalized:
177
+ member_names.add(normalized)
178
+
179
+ # berry always records what it installed, so an empty result means this is not a workspace
180
+ return frozenset(member_names)
181
+
182
+
183
+ class WorkspaceMemberResolver(ABC):
184
+ """Reads, from one root lockfile, the member directories that lockfile resolves."""
185
+
186
+ @property
187
+ @abstractmethod
188
+ def package_manager(self) -> str: ...
189
+
190
+ @property
191
+ @abstractmethod
192
+ def lock_file_names(self) -> tuple: ...
193
+
194
+ @property
195
+ def may_use_workspace_globs(self) -> bool:
196
+ """Whether an unanswerable lockfile may defer to the manifest's workspaces globs.
197
+
198
+ False means the silence is itself an answer: a v1 package-lock predates workspaces, so
199
+ its root is simply not one. True means the format has workspaces but does not record
200
+ them, leaving the globs as the only remaining source.
201
+ """
202
+ return False
203
+
204
+ def resolve(self, lock_file: Path) -> Optional[frozenset]:
205
+ """Member paths this lockfile resolves, or None when it cannot name them.
206
+
207
+ Caches on the file's identity so one scan parses each root lockfile once.
208
+ """
209
+ stamp = file_stamp(lock_file)
210
+ if stamp is None:
211
+ return None
212
+
213
+ if stamp in _member_names_cache:
214
+ return _member_names_cache[stamp]
215
+
216
+ member_names = self._read_member_names(lock_file)
217
+ _member_names_cache[stamp] = member_names
218
+ return member_names
219
+
220
+ @abstractmethod
221
+ def _read_member_names(self, lock_file: Path) -> Optional[frozenset]: ...
222
+
223
+
224
+ class NpmLockfileResolver(WorkspaceMemberResolver):
225
+ package_manager = NPM_PACKAGE_MANAGER
226
+ lock_file_names = (NPM_LOCK_FILE_NAME, NPM_SHRINKWRAP_FILE_NAME)
227
+
228
+ def _read_member_names(self, lock_file: Path) -> Optional[frozenset]:
229
+ return _npm_lockfile_member_names(lock_file)
230
+
231
+
232
+ class PnpmLockfileResolver(WorkspaceMemberResolver):
233
+ package_manager = PNPM_PACKAGE_MANAGER
234
+ lock_file_names = (PNPM_LOCK_FILE_NAME,)
235
+
236
+ def _read_member_names(self, lock_file: Path) -> Optional[frozenset]:
237
+ return _pnpm_lockfile_member_names(lock_file)
238
+
239
+
240
+ class YarnLockfileResolver(WorkspaceMemberResolver):
241
+ """Berry names every member; classic is flat and names none, so only classic needs the globs."""
242
+
243
+ package_manager = YARN_PACKAGE_MANAGER
244
+ lock_file_names = (YARN_LOCK_FILE_NAME,)
245
+ may_use_workspace_globs = True
246
+
247
+ def _read_member_names(self, lock_file: Path) -> Optional[frozenset]:
248
+ return _yarn_lockfile_member_names(lock_file)
249
+
250
+
251
+ class BunLockfileResolver(WorkspaceMemberResolver):
252
+ """Only the text bun.lock names members; the binary bun.lockb is handled as opaque."""
253
+
254
+ package_manager = BUN_PACKAGE_MANAGER
255
+ lock_file_names = (BUN_LOCK_FILE_NAME,)
256
+
257
+ def _read_member_names(self, lock_file: Path) -> Optional[frozenset]:
258
+ return _bun_lockfile_member_names(lock_file)
259
+
260
+
261
+ class OpaqueLockfileResolver(WorkspaceMemberResolver):
262
+ """A lockfile we cannot read members from at all, so the manifest globs decide."""
263
+
264
+ may_use_workspace_globs = True
265
+
266
+ def __init__(self, package_manager: str, lock_file_names: tuple) -> None:
267
+ self._package_manager = package_manager
268
+ self._lock_file_names = lock_file_names
269
+
270
+ @property
271
+ def package_manager(self) -> str:
272
+ return self._package_manager
273
+
274
+ @property
275
+ def lock_file_names(self) -> tuple:
276
+ return self._lock_file_names
277
+
278
+ def _read_member_names(self, lock_file: Path) -> Optional[frozenset]:
279
+ return None
280
+
281
+
282
+ # Order is precedence: the first lockfile that resolves the member wins.
283
+ #
284
+ # deno.lock is deliberately absent. The glob fallback reads the manifest's workspaces field,
285
+ # which deno does not use - it declares members in deno.json - so a match there would be
286
+ # meaningless. A deno.lock beside a manifest still stops the npm fallback, through the
287
+ # alternative-lockfile guard in the npm handler.
288
+ MEMBER_RESOLVERS = (
289
+ NpmLockfileResolver(),
290
+ YarnLockfileResolver(),
291
+ PnpmLockfileResolver(),
292
+ BunLockfileResolver(),
293
+ OpaqueLockfileResolver(BUN_PACKAGE_MANAGER, (BUN_BINARY_LOCK_FILE_NAME,)),
294
+ )
@@ -15,6 +15,7 @@ from cycode.cli.files_collector.sca.npm.restore_deno_dependencies import Restore
15
15
  from cycode.cli.files_collector.sca.npm.restore_npm_dependencies import RestoreNpmDependencies
16
16
  from cycode.cli.files_collector.sca.npm.restore_pnpm_dependencies import RestorePnpmDependencies
17
17
  from cycode.cli.files_collector.sca.npm.restore_yarn_dependencies import RestoreYarnDependencies
18
+ from cycode.cli.files_collector.sca.npm.workspace import clear_cache as clear_npm_workspace_cache
18
19
  from cycode.cli.files_collector.sca.nuget.restore_nuget_dependencies import RestoreNugetDependencies
19
20
  from cycode.cli.files_collector.sca.php.restore_composer_dependencies import RestoreComposerDependencies
20
21
  from cycode.cli.files_collector.sca.python.restore_pip_dependencies import RestorePipDependencies
@@ -186,6 +187,8 @@ def _add_dependencies_tree_documents(
186
187
  {'documents_count': len(documents_to_scan), 'is_git_diff': is_git_diff},
187
188
  )
188
189
 
190
+ clear_npm_workspace_cache()
191
+
189
192
  documents_to_add: dict[str, Document] = {document.path: document for document in documents_to_scan}
190
193
  restore_dependencies_list = _get_restore_handlers(ctx, is_git_diff)
191
194
 
@@ -152,11 +152,38 @@ def concat_unique_id(filename: str, unique_id: str) -> str:
152
152
  return str(PurePosixPath(unique_id, *_to_relative_posix_parts(filename)))
153
153
 
154
154
 
155
+ def _as_scan_root(value: object) -> Optional[str]:
156
+ if isinstance(value, (str, os.PathLike)):
157
+ return os.fspath(value) or None
158
+
159
+ return None
160
+
161
+
162
+ def get_scan_roots_from_context(ctx: typer.Context) -> tuple[str, ...]:
163
+ """Every path the user asked to scan, in the order they were given.
164
+
165
+ Typer declares these arguments as Path, so callers must not assume str.
166
+ """
167
+ params = getattr(ctx, 'params', None)
168
+ if not isinstance(params, dict):
169
+ return ()
170
+
171
+ scan_roots = []
172
+
173
+ single_root = _as_scan_root(params.get('path'))
174
+ if single_root:
175
+ scan_roots.append(single_root)
176
+
177
+ paths = params.get('paths')
178
+ if isinstance(paths, (list, tuple)):
179
+ scan_roots.extend(root for root in (_as_scan_root(entry) for entry in paths) if root)
180
+
181
+ return tuple(dict.fromkeys(scan_roots))
182
+
183
+
155
184
  def get_path_from_context(ctx: typer.Context) -> Optional[str]:
156
- path = ctx.params.get('path')
157
- if path is None and 'paths' in ctx.params:
158
- path = ctx.params['paths'][0]
159
- return path
185
+ scan_roots = get_scan_roots_from_context(ctx)
186
+ return scan_roots[0] if scan_roots else None
160
187
 
161
188
 
162
189
  def normalize_file_path(path: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.25.1.dev2
3
+ Version: 3.25.1.dev4
4
4
  Summary: Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.
5
5
  License-Expression: MIT
6
6
  License-File: LICENCE
@@ -24,7 +24,7 @@ Requires-Dist: anyio (>=4.0.0,<4.13.0)
24
24
  Requires-Dist: arrow (>=1.0.0,<1.5.0)
25
25
  Requires-Dist: click (>=8.1.0,<8.2.0)
26
26
  Requires-Dist: colorama (>=0.4.3,<0.5.0)
27
- Requires-Dist: gitpython (>=3.1.59,<3.2.0)
27
+ Requires-Dist: gitpython (>=3.1.62,<3.2.0)
28
28
  Requires-Dist: marshmallow (>=4.0.1,<4.1.0)
29
29
  Requires-Dist: mcp (>=1.28.1,<2.0.0) ; python_version >= "3.10"
30
30
  Requires-Dist: patch-ng (==1.19.1)
@@ -1,4 +1,4 @@
1
- cycode/__init__.py,sha256=d_9xB5xWmsLjV8KowoBInTpCeZkode1JD9Que7AnzrM,396
1
+ cycode/__init__.py,sha256=maSJqqkxg3160B3eJEs3M1LWs_m8n1e6k8181bbO1IA,396
2
2
  cycode/__main__.py,sha256=Z3bD5yrA7yPvAChcADQrqCaZd0ChGI1gdiwALwbWJ6U,104
3
3
  cycode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  cycode/cli/app.py,sha256=AlR2durAEbsa47PDfIj7JtMvJDWA_Dq6wPtVuMJYSCs,10250
@@ -124,11 +124,17 @@ cycode/cli/files_collector/sca/maven/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeu
124
124
  cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py,sha256=2W5Y8yrSIV7o4rCUtNsqO2YFImvcY5TnabzTbStiaBo,3261
125
125
  cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py,sha256=zObNN8n6yUriNVB3ZdvAkoKXXrMvzU9Lpd5lNhVo_so,4003
126
126
  cycode/cli/files_collector/sca/npm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
- cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py,sha256=HI0X_19wCM6B_vRpX6uD12dXsQbU6LCcLQdobPZDgAs,4580
128
- cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py,sha256=XL0VXEPL0jf6ruZZkCpv99lkU8-MNc09CU1fgGgTbHs,1768
129
- cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py,sha256=Uwoemv18PX2_9pSGzyYJ7wixQtgDZVOuuJuHIjdnF7M,3173
130
- cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py,sha256=CcvJOox2JpWl_UeCjMP9I8fP85bPZXfhH7IwLzq2U8E,2708
131
- cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py,sha256=903Ra2YOKwlJnknG47embjPLQUajUb8_1j94GAVcnX0,2698
127
+ cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py,sha256=65mrzCwQTLpm7f5VUxLqIP_yiX5BFj66WM85Z4W2H9E,4857
128
+ cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py,sha256=II3JBROuh1cqPHHsFYU78ej-mUIgqN9iwVB-czWAtIo,1811
129
+ cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py,sha256=zRoXhkJnqaic3Bd7_3rqtzqP404kyP2NuJM8LpgfePk,4023
130
+ cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py,sha256=FkYpbA1cFKHHudhedAzxGIm1L2oBB-mtxQHAB6O0ATs,2979
131
+ cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py,sha256=qy-5uEBkw5c4xmjb5r6Cvx84XeS7fxdWcWSHIixM38w,2974
132
+ cycode/cli/files_collector/sca/npm/workspace/__init__.py,sha256=G0b_a6ZrgOnqpicf0ZvXZ4Zvs5LH-uBkqi8LzRigl7s,1611
133
+ cycode/cli/files_collector/sca/npm/workspace/coverage.py,sha256=JLpsASXCiTgdFZX5xFN1jPqplFSGMU05qG28hJiXXTw,5039
134
+ cycode/cli/files_collector/sca/npm/workspace/files.py,sha256=uEnqs0TBRfaQ0C_Elqj1lzoNIU-mG4hqN_fS3g3Y59Q,1035
135
+ cycode/cli/files_collector/sca/npm/workspace/globs.py,sha256=TBh6UHHN4Z391Ap3TmANJ5HrPLWcOnQuitoCm2Amoz0,4231
136
+ cycode/cli/files_collector/sca/npm/workspace/names.py,sha256=mL6lp73EFemCuHqeg1GoDHa6GR0cH52sjcfmkgpaXVs,510
137
+ cycode/cli/files_collector/sca/npm/workspace/resolvers.py,sha256=5WhhlACE9NYaQRyfOfFM64ScruQ7bvORlbmuU7J8QQI,10295
132
138
  cycode/cli/files_collector/sca/nuget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
139
  cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py,sha256=wUsuO_JusyIPTBfocHAr48L7rdYtP1B86N0nS4FXdXg,1123
134
140
  cycode/cli/files_collector/sca/php/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -142,7 +148,7 @@ cycode/cli/files_collector/sca/ruby/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQ
142
148
  cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py,sha256=Vqswcxte9YjGnvIm9oZ8r91jNyhuiYDf1mouaTaLg3U,694
143
149
  cycode/cli/files_collector/sca/sbt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
144
150
  cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py,sha256=cfBUR4iQcfplX_O2QPjYh2wSyBteze8wZcT_dG9d1d4,709
145
- cycode/cli/files_collector/sca/sca_file_collector.py,sha256=yqrWw_1FE3Q-KJJj756GTl36bcYa5pr7O6yByebyMng,10764
151
+ cycode/cli/files_collector/sca/sca_file_collector.py,sha256=IKDV-rEXTse7bQuJapKWN_mWlGjug2mkAbb3KyFYpkE,10895
146
152
  cycode/cli/files_collector/walk_ignore.py,sha256=nvOM6oDmT2SxSI4pU-bLlc9LwTgkfTd2egse69ixf3g,2464
147
153
  cycode/cli/files_collector/zip_documents.py,sha256=FMzbA2Vog7Zl_ntizNQJK8AFqoGu0QlPIMIBpgmBiVI,1852
148
154
  cycode/cli/logger.py,sha256=mlaYEQGYd582fTCc3SC3cFMj0PKTB6EsaI12Q4VL1z8,65
@@ -183,7 +189,7 @@ cycode/cli/utils/git_proxy.py,sha256=FPHMBiyLFK9X9vKYpKySRKJH6Dc9Cb3nO241Q95dASE
183
189
  cycode/cli/utils/host_info.py,sha256=Clln3qCJR00rw6HndD0bAIv329n_bOEx42TKW6FqXFQ,6379
184
190
  cycode/cli/utils/ignore_utils.py,sha256=cODqhnOHA2kRo8rMY0YcmcKkmXNPOC9UTCmFu62RRqE,15567
185
191
  cycode/cli/utils/jwt_utils.py,sha256=EGI-0CKhCGY8hIcZ9b9diq9hqtOUf8Ha8ukeVJIf974,818
186
- cycode/cli/utils/path_utils.py,sha256=YlAUz2seaKT25ggqcnAjxqQd1oUlXTn1LIebBOht4nQ,5699
192
+ cycode/cli/utils/path_utils.py,sha256=lRFVhmzW1XGoC7U-_-2Lx_vfiSgo1IhGCEfARW3Y6oQ,6493
187
193
  cycode/cli/utils/progress_bar.py,sha256=bKBWHHdZsVkdDdWMJLfgLGR0cBYeB44P_DpRM8pvWqU,9528
188
194
  cycode/cli/utils/scan_batch.py,sha256=5xKGVDVqoRxdKhuZkK11x4QrNqKmU20Q83E_fy8Nndk,5188
189
195
  cycode/cli/utils/scan_utils.py,sha256=_VkZ7maLVST6J8dsqDsKYWQj2EVfqI3iPPsA1QJOGEU,1259
@@ -217,8 +223,8 @@ cycode/cyclient/report_client.py,sha256=Scq30NeJPzgXv0hPLO1U05AdE9i_2iu6cIrSKpEJ
217
223
  cycode/cyclient/scan_client.py,sha256=DqAZ7u6Z_cvw9A9RlLkAQUgLRwPCCAsUq5U9umt4F7Y,16955
218
224
  cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
219
225
  cycode/logger.py,sha256=EfZGRK6VC5rE_LAjIcRrHFiQCueylCDXoG6bvGkrIME,2111
220
- cycode-3.25.1.dev2.dist-info/METADATA,sha256=uIOqTMO1qWH2j7PLfL40kasRHo4xw8phk2PzHmk4iLk,96824
221
- cycode-3.25.1.dev2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
222
- cycode-3.25.1.dev2.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
223
- cycode-3.25.1.dev2.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
224
- cycode-3.25.1.dev2.dist-info/RECORD,,
226
+ cycode-3.25.1.dev4.dist-info/METADATA,sha256=Lf-JTQlrq2jGCZcRrGL1uQJexg0UkInWT_mOjm7A5bk,96824
227
+ cycode-3.25.1.dev4.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
228
+ cycode-3.25.1.dev4.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
229
+ cycode-3.25.1.dev4.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
230
+ cycode-3.25.1.dev4.dist-info/RECORD,,