socketsecurity 2.4.6__tar.gz → 2.4.7__tar.gz

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 (145) hide show
  1. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/CHANGELOG.md +21 -0
  2. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/PKG-INFO +1 -1
  3. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/docs/cli-reference.md +13 -8
  4. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/pyproject.toml +1 -1
  5. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/__init__.py +1 -1
  6. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/config.py +4 -2
  7. socketsecurity-2.4.7/socketsecurity/core/tools/reachability.py +469 -0
  8. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/socketcli.py +1 -1
  9. socketsecurity-2.4.7/tests/unit/test_reachability.py +384 -0
  10. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/uv.lock +1 -1
  11. socketsecurity-2.4.6/socketsecurity/core/tools/reachability.py +0 -330
  12. socketsecurity-2.4.6/tests/unit/test_reachability.py +0 -106
  13. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/CODEOWNERS +0 -0
  14. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  15. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  16. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  17. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/actions/setup-docker/action.yml +0 -0
  19. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/actions/setup-hatch/action.yml +0 -0
  20. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/actions/setup-sfw/action.yml +0 -0
  21. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/dependabot.yml +0 -0
  22. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/workflows/dependency-review.yml +0 -0
  23. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/workflows/docker-stable.yml +0 -0
  24. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/workflows/e2e-test.yml +0 -0
  25. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/workflows/pr-preview.yml +0 -0
  26. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/workflows/python-tests.yml +0 -0
  27. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/workflows/release.yml +0 -0
  28. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/workflows/version-check.yml +0 -0
  29. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.github/zizmor.yml +0 -0
  30. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.gitignore +0 -0
  31. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.hooks/sync_version.py +0 -0
  32. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.pre-commit-config.yaml +0 -0
  33. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/.python-version +0 -0
  34. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/Dockerfile +0 -0
  35. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/LICENSE +0 -0
  36. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/Makefile +0 -0
  37. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/README.md +0 -0
  38. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/docs/ci-cd.md +0 -0
  39. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/docs/development.md +0 -0
  40. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/docs/troubleshooting.md +0 -0
  41. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/examples/config/sarif-dashboard-parity.json +0 -0
  42. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/examples/config/sarif-dashboard-parity.toml +0 -0
  43. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/examples/config/sarif-diff-ci-cd.json +0 -0
  44. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/examples/config/sarif-diff-ci-cd.toml +0 -0
  45. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/examples/config/sarif-instance-detail.json +0 -0
  46. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/examples/config/sarif-instance-detail.toml +0 -0
  47. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/instructions/gitlab-commit-status/uat.md +0 -0
  48. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/pytest.ini +0 -0
  49. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/scripts/build_container.sh +0 -0
  50. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/scripts/build_container_flexible.sh +0 -0
  51. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/scripts/deploy-test-docker.sh +0 -0
  52. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/scripts/deploy-test-pypi.sh +0 -0
  53. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/scripts/docker-entrypoint.sh +0 -0
  54. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/scripts/run.sh +0 -0
  55. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/session.md +0 -0
  56. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socket.yml +0 -0
  57. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/__init__.py +0 -0
  58. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/alert_selection.py +0 -0
  59. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/classes.py +0 -0
  60. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/cli_client.py +0 -0
  61. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/exceptions.py +0 -0
  62. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/git_interface.py +0 -0
  63. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/helper/__init__.py +0 -0
  64. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
  65. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/lazy_file_loader.py +0 -0
  66. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/logging.py +0 -0
  67. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/messages.py +0 -0
  68. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/resource_utils.py +0 -0
  69. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/scm/__init__.py +0 -0
  70. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/scm/base.py +0 -0
  71. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/scm/client.py +0 -0
  72. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/scm/github.py +0 -0
  73. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/scm/gitlab.py +0 -0
  74. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/scm_comments.py +0 -0
  75. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/socket_config.py +0 -0
  76. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/core/utils.py +0 -0
  77. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/fossa_compat.py +0 -0
  78. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/output.py +0 -0
  79. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/__init__.py +0 -0
  80. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/base.py +0 -0
  81. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/formatters/__init__.py +0 -0
  82. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/formatters/slack.py +0 -0
  83. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/jira.py +0 -0
  84. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/manager.py +0 -0
  85. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/slack.py +0 -0
  86. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/teams.py +0 -0
  87. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/socketsecurity/plugins/webhook.py +0 -0
  88. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/__init__.py +0 -0
  89. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/conftest.py +0 -0
  90. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/create_diff_input.json +0 -0
  91. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/test_diff_alerts.py +0 -0
  92. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/test_diff_generation.py +0 -0
  93. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/test_facts_compression.py +0 -0
  94. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/test_has_manifest_files.py +0 -0
  95. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/test_package_and_alerts.py +0 -0
  96. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/test_sdk_methods.py +0 -0
  97. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/core/test_supporting_methods.py +0 -0
  98. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/create_response.json +0 -0
  99. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/diff/stream_diff.json +0 -0
  100. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  101. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/head_scan/metadata.json +0 -0
  102. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  103. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  104. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/new_scan/metadata.json +0 -0
  105. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  106. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/repos/repo_info_error.json +0 -0
  107. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/repos/repo_info_no_head.json +0 -0
  108. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/repos/repo_info_success.json +0 -0
  109. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/data/settings/security-policy.json +0 -0
  110. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/fixtures/simple-npm/index.js +0 -0
  111. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/fixtures/simple-npm/package.json +0 -0
  112. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
  113. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/validate-gitlab.sh +0 -0
  114. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/validate-json.sh +0 -0
  115. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/validate-reachability.sh +0 -0
  116. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/validate-sarif.sh +0 -0
  117. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/e2e/validate-scan.sh +0 -0
  118. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/fixtures/fossa/README.md +0 -0
  119. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-analyze-empty.json +0 -0
  120. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-analyze-populated.json +0 -0
  121. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-sbom-empty-deep.json +0 -0
  122. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-sbom-populated.json +0 -0
  123. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/__init__.py +0 -0
  124. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_alert_selection.py +0 -0
  125. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_cli_config.py +0 -0
  126. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_client.py +0 -0
  127. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_config.py +0 -0
  128. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_dependency_overview.py +0 -0
  129. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_disable_ignore.py +0 -0
  130. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_exclude_paths.py +0 -0
  131. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_fossa_compat.py +0 -0
  132. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_fossa_parity.py +0 -0
  133. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_gitlab_auth.py +0 -0
  134. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  135. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_gitlab_commit_status.py +0 -0
  136. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_gitlab_format.py +0 -0
  137. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_ignore_telemetry_filtering.py +0 -0
  138. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_output.py +0 -0
  139. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_slack_plugin.py +0 -0
  140. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_socketcli.py +0 -0
  141. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/tests/unit/test_tier1_finalize.py +0 -0
  142. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/workflows/bitbucket-pipelines.yml +0 -0
  143. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/workflows/buildkite.yml +0 -0
  144. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/workflows/github-actions.yml +0 -0
  145. {socketsecurity-2.4.6 → socketsecurity-2.4.7}/workflows/gitlab-ci.yml +0 -0
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.4.7
4
+
5
+ ### Changed: pin @coana-tech/cli version; auto-update is now opt-in
6
+
7
+ - Reachability analysis now runs a fixed `@coana-tech/cli` version pinned to this CLI release
8
+ (`15.3.24`) via `npx`, instead of silently pulling the latest published version on every run.
9
+ Engine version changes now ride with the Socket Python CLI release (standard `pip` upgrade),
10
+ giving advance notice of analysis-engine changes.
11
+ - The CLI no longer runs `npm install -g @coana-tech/cli`; an existing global install is left
12
+ untouched (never auto-updated or downgraded).
13
+ - Opt into always-newest with `--reach-version latest`; pin an explicit version with
14
+ `--reach-version <semver>` (unchanged).
15
+ - Runs the engine via `npx --yes --force` (the same flags the Socket Node CLI passes for
16
+ coana); `--yes` skips npx's interactive install prompt so non-interactive/CI runs don't hang.
17
+ - Added an `npm install` + `node` fallback for when the `npx` launcher is missing or fails
18
+ before the engine starts. The installed engine is cached per version for the process
19
+ lifetime (installs once). Tunable via `SOCKET_CLI_COANA_FORCE_NPM_INSTALL` (use the fallback
20
+ as the primary path) and `SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK` (never fall back). `node` is
21
+ now part of the up-front prerequisite check. Also strips `npm_package_*` env vars before
22
+ spawning the engine to avoid `E2BIG` in large monorepos.
23
+
3
24
  ## 2.4.6
4
25
 
5
26
  ### Docs: reachability reference corrections
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketsecurity
3
- Version: 2.4.6
3
+ Version: 2.4.7
4
4
  Summary: Socket Security CLI for CI/CD
5
5
  Project-URL: Homepage, https://socket.dev
6
6
  Author-email: Douglas Coburn <douglas@socket.dev>
@@ -240,13 +240,13 @@ If you don't want to provide the Socket API Token every time then you can use th
240
240
  | Parameter | Required | Default | Description |
241
241
  |:---------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------|
242
242
  | `--reach` | False | False | Enable reachability analysis to identify which vulnerable functions are actually called by your code. Creates a tier-1 full-application reachability scan (`scan_type=socket_tier1`). |
243
- | `--reach-version` | False | latest | Version of @coana-tech/cli to use for analysis |
244
- | `--reach-analysis-timeout` | False | *coana* | Timeout in seconds for the reachability analysis. Omitted by default, so coana applies its own (currently 600s). Alias: `--reach-timeout` |
245
- | `--reach-analysis-memory-limit` | False | *coana* | Memory limit in MB for the reachability analysis. Omitted by default, so coana applies its own (currently 8192). Alias: `--reach-memory-limit` |
246
- | `--reach-concurrency` | False | *coana* | Control parallel analysis execution (must be >= 1). Omitted by default, so coana applies its own (currently 1) |
243
+ | `--reach-version` | False | 15.3.24 | Version of @coana-tech/cli to use. Defaults to the pinned version that ships with this CLI release, so the engine only changes when you upgrade the Socket CLI. Pass `latest` to always use the newest published version (opt-in auto-update), or an explicit version (e.g. `1.2.3`) to pin it. |
244
+ | `--reach-analysis-timeout` | False | 600 | Timeout in seconds for the reachability analysis. Omitted by default, so coana applies its own default. Alias: `--reach-timeout` |
245
+ | `--reach-analysis-memory-limit` | False | 8192 | Memory limit in MB for the reachability analysis. Omitted by default, so coana applies its own default. Alias: `--reach-memory-limit` |
246
+ | `--reach-concurrency` | False | 1 | Control parallel analysis execution (must be >= 1). Omitted by default, so coana applies its own default. |
247
247
  | `--reach-additional-params` | False | | Pass custom parameters to the coana CLI tool |
248
248
  | `--reach-ecosystems` | False | | Comma-separated list of ecosystems to analyze (e.g., "npm,pypi"). If not specified, all supported ecosystems are analyzed |
249
- | `--reach-min-severity` | False | | Minimum severity level for reporting reachability results (info, low, moderate, high, critical) |
249
+ | `--reach-min-severity` | False | info | Minimum severity of vulnerabilities to analyze (info, low, moderate, high, critical). Omitted by default, so coana analyzes all severities — equivalent to `info`, the lowest. |
250
250
  | `--reach-skip-cache` | False | False | Skip cache and force fresh reachability analysis |
251
251
  | `--reach-disable-analytics` | False | False | Disable analytics collection during reachability analysis |
252
252
  | `--reach-enable-analysis-splitting` | False | False | Enable analysis splitting/bucketing (a legacy performance feature). Splitting is disabled by default. |
@@ -262,8 +262,9 @@ If you don't want to provide the Socket API Token every time then you can use th
262
262
  **Reachability Analysis Requirements:**
263
263
 
264
264
  The Python CLI verifies the following **up front** (before invoking the analysis engine) and exits with code **3** if any are unmet:
265
- - `npm` - Required to install and run `@coana-tech/cli` (the analysis engine)
266
- - `npx` - Required to execute `@coana-tech/cli`
265
+ - `npm` - Required (verified up front; ships alongside `npx`)
266
+ - `npx` - Required to fetch (on first use) and run `@coana-tech/cli` (the analysis engine)
267
+ - `node` - Required to run the engine (used directly by the `npm install` fallback)
267
268
  - `uv` - Required by the analysis engine
268
269
  - An **Enterprise** Socket organization plan (any `enterprise*` plan, including Enterprise trials)
269
270
 
@@ -313,7 +314,11 @@ Sample config files:
313
314
 
314
315
  For CI-specific examples and guidance, see [`ci-cd.md`](ci-cd.md).
315
316
 
316
- The CLI will automatically install `@coana-tech/cli` if not present. Use `--reach` to enable reachability analysis during a full scan, or add `--only-facts-file` (with `--reach`) to submit only the reachability facts file (`.socket.facts.json`) when creating the full scan.
317
+ The CLI runs a pinned `@coana-tech/cli` version via `npx --yes --force` (the same flags the Socket Node CLI passes for coana); it does **not** auto-update the engine or install it globally. `--yes` skips npx's interactive install prompt so non-interactive/CI runs don't hang. If the `npx` launcher is unavailable or fails before the engine starts, the CLI falls back to `npm install`-ing the pinned version into a temp directory and running it via `node`. Pass `--reach-version latest` to opt into the newest published version. Use `--reach` to enable reachability analysis during a full scan, or add `--only-facts-file` (with `--reach`) to submit only the reachability facts file (`.socket.facts.json`) when creating the full scan.
318
+
319
+ The launcher fallback can be tuned via environment variables:
320
+ - `SOCKET_CLI_COANA_FORCE_NPM_INSTALL` — skip `npx` entirely and always use the `npm install` + `node` path (useful where `npx` is known-broken).
321
+ - `SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK` — never fall back; surface the `npx` failure directly.
317
322
 
318
323
  #### Advanced Configuration
319
324
  | Parameter | Required | Default | Description |
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.4.6"
9
+ version = "2.4.7"
10
10
  requires-python = ">= 3.11"
11
11
  license = {"file" = "LICENSE"}
12
12
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.4.6'
2
+ __version__ = '2.4.7'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -943,8 +943,10 @@ def create_argument_parser() -> argparse.ArgumentParser:
943
943
  reachability_group.add_argument(
944
944
  "--reach-version",
945
945
  dest="reach_version",
946
- metavar="<version>",
947
- help="Specific version of @coana-tech/cli to use (e.g., '1.2.3')"
946
+ metavar="<version|latest>",
947
+ help="Version of @coana-tech/cli to use. Defaults to the version pinned to this CLI "
948
+ "release; pass 'latest' to always use the newest published version (opt-in "
949
+ "auto-update), or an explicit version (e.g. '1.2.3') to pin it."
948
950
  )
949
951
  reachability_group.add_argument(
950
952
  "--reach-analysis-timeout",
@@ -0,0 +1,469 @@
1
+ from socketdev import socketdev
2
+ from typing import List, Optional, Dict, Any, Final
3
+ import atexit
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import subprocess
8
+ import json
9
+ import pathlib
10
+ import logging
11
+ import sys
12
+ import tempfile
13
+
14
+ from socketsecurity import __version__
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+ # Pinned @coana-tech/cli version. Bumped deliberately per Python CLI release so the
19
+ # reachability engine version only changes through a standard pip upgrade (advance notice).
20
+ # Pass --reach-version latest to opt into the newest published version instead.
21
+ DEFAULT_COANA_CLI_VERSION: Final = "15.3.24"
22
+
23
+ # Resolved @coana-tech/cli script paths from the npm-install fallback, keyed by version.
24
+ # Lives for the process lifetime so repeated fallback invocations install only once
25
+ # (mirrors the Node CLI's installedCoanaScriptPathsByVersion).
26
+ _INSTALLED_COANA_SCRIPT_PATHS: Dict[str, str] = {}
27
+
28
+ # Temp dirs created by the npm-install fallback, removed at process exit.
29
+ _COANA_INSTALL_DIRS: List[str] = []
30
+
31
+
32
+ @atexit.register
33
+ def _cleanup_coana_install_dirs() -> None:
34
+ for install_dir in _COANA_INSTALL_DIRS:
35
+ shutil.rmtree(install_dir, ignore_errors=True)
36
+
37
+
38
+ def _build_caller_user_agent() -> str:
39
+ """Build the SOCKET_CALLER_USER_AGENT string forwarded to the coana CLI.
40
+
41
+ Mirrors the Node CLI's ``<product>/<version> <runtime>/<version> <platform>/<arch>``
42
+ shape so the backend can attribute reachability calls to the Python CLI.
43
+ """
44
+ return (
45
+ f"socket/{__version__} "
46
+ f"python/{platform.python_version()} "
47
+ f"{platform.system().lower()}/{platform.machine().lower()}"
48
+ )
49
+
50
+
51
+ class ReachabilityAnalyzer:
52
+ def __init__(self, sdk: socketdev, api_token: str):
53
+ self.sdk = sdk
54
+ self.api_token = api_token
55
+
56
+ def _resolve_coana_package_spec(self, version: Optional[str] = None) -> str:
57
+ """
58
+ Resolve the @coana-tech/cli package spec to run (e.g. '@coana-tech/cli@15.3.24').
59
+
60
+ Args:
61
+ version: Coana CLI version to use.
62
+ - None: the pinned ``DEFAULT_COANA_CLI_VERSION`` (no auto-update).
63
+ - 'latest': always the newest published version (opt-in to auto-update).
64
+ - '<semver>': that exact version.
65
+
66
+ Returns:
67
+ str: The package specifier to use with npx (e.g. '@coana-tech/cli@15.3.24').
68
+ """
69
+ return f"@coana-tech/cli@{self._resolve_coana_version(version)}"
70
+
71
+ def _resolve_coana_version(self, version: Optional[str] = None) -> str:
72
+ """Resolve the effective @coana-tech/cli version string (see _resolve_coana_package_spec)."""
73
+ return (version or DEFAULT_COANA_CLI_VERSION).strip()
74
+
75
+
76
+ def run_reachability_analysis(
77
+ self,
78
+ org_slug: str,
79
+ target_directory: str,
80
+ tar_hash: Optional[str] = None,
81
+ output_path: str = ".socket.facts.json",
82
+ timeout: Optional[int] = None,
83
+ memory_limit: Optional[int] = None,
84
+ ecosystems: Optional[List[str]] = None,
85
+ exclude_paths: Optional[List[str]] = None,
86
+ min_severity: Optional[str] = None,
87
+ skip_cache: bool = False,
88
+ disable_analytics: bool = False,
89
+ enable_analysis_splitting: bool = False,
90
+ detailed_analysis_log_file: bool = False,
91
+ lazy_mode: bool = False,
92
+ repo_name: Optional[str] = None,
93
+ branch_name: Optional[str] = None,
94
+ version: Optional[str] = None,
95
+ concurrency: Optional[int] = None,
96
+ additional_params: Optional[List[str]] = None,
97
+ allow_unverified: bool = False,
98
+ enable_debug: bool = False,
99
+ use_only_pregenerated_sboms: bool = False,
100
+ continue_on_analysis_errors: bool = False,
101
+ continue_on_install_errors: bool = False,
102
+ continue_on_missing_lock_files: bool = False,
103
+ continue_on_no_source_files: bool = False,
104
+ reach_debug: bool = False,
105
+ disable_external_tool_checks: bool = False,
106
+ ) -> Dict[str, Any]:
107
+ """
108
+ Run reachability analysis.
109
+
110
+ Args:
111
+ org_slug: Socket organization slug
112
+ target_directory: Directory to analyze
113
+ tar_hash: Tar hash from manifest upload or existing scan (optional)
114
+ output_path: Output file path for results
115
+ timeout: Analysis timeout in seconds
116
+ memory_limit: Memory limit in MB
117
+ ecosystems: List of ecosystems to analyze (e.g., ['npm', 'pypi'])
118
+ exclude_paths: Paths to exclude from analysis
119
+ min_severity: Minimum severity level (info, low, moderate, high, critical)
120
+ skip_cache: Skip cache usage
121
+ disable_analytics: Disable analytics sharing
122
+ enable_analysis_splitting: Enable analysis splitting (disabled by default)
123
+ detailed_analysis_log_file: Print detailed analysis log file path
124
+ lazy_mode: Enable lazy mode for analysis
125
+ repo_name: Repository name
126
+ branch_name: Branch name
127
+ version: @coana-tech/cli version to use. None uses the pinned
128
+ DEFAULT_COANA_CLI_VERSION (no auto-update); 'latest' opts into the newest
129
+ published version; '<semver>' pins an explicit version.
130
+ concurrency: Concurrency level for analysis (must be >= 1)
131
+ additional_params: Additional parameters to pass to coana CLI
132
+ allow_unverified: Disable SSL certificate verification (sets NODE_TLS_REJECT_UNAUTHORIZED=0)
133
+ enable_debug: Enable debug mode (passes -d flag to coana CLI)
134
+ use_only_pregenerated_sboms: Use only pre-generated CDX and SPDX files for the scan
135
+
136
+ Returns:
137
+ Dict containing scan_id and report_path
138
+ """
139
+ # Build the coana CLI arguments (everything after the package spec). The launcher
140
+ # (npx, or the npm-install + node fallback) is chosen in _spawn_coana() below.
141
+ coana_args = ["run", "."]
142
+
143
+ # Add required arguments
144
+ output_dir = str(pathlib.Path(output_path).parent)
145
+ log.debug(f"output_dir: {output_dir}, output_path: {output_path}")
146
+ coana_args.extend([
147
+ "--output-dir", output_dir,
148
+ "--socket-mode", output_path,
149
+ "--disable-report-submission"
150
+ ])
151
+
152
+ # Add conditional arguments
153
+ if timeout:
154
+ coana_args.extend(["--analysis-timeout", str(timeout)])
155
+
156
+ if memory_limit:
157
+ coana_args.extend(["--memory-limit", str(memory_limit)])
158
+
159
+ if disable_analytics:
160
+ coana_args.append("--disable-analytics-sharing")
161
+
162
+ # Analysis splitting is disabled by default; only omit the flag if explicitly enabled
163
+ if not enable_analysis_splitting:
164
+ coana_args.append("--disable-analysis-splitting")
165
+
166
+ if detailed_analysis_log_file:
167
+ coana_args.append("--print-analysis-log-file")
168
+
169
+ if lazy_mode:
170
+ coana_args.append("--lazy-mode")
171
+
172
+ # KEY POINT: Only add manifest tar hash if we have one
173
+ if tar_hash:
174
+ coana_args.extend(["--run-without-docker", "--manifests-tar-hash", tar_hash])
175
+
176
+ if ecosystems:
177
+ coana_args.extend(["--purl-types"] + ecosystems)
178
+
179
+ if exclude_paths:
180
+ coana_args.extend(["--exclude-dirs"] + exclude_paths)
181
+
182
+ if min_severity:
183
+ coana_args.extend(["--min-severity", min_severity])
184
+
185
+ if skip_cache:
186
+ coana_args.append("--skip-cache-usage")
187
+
188
+ if concurrency:
189
+ coana_args.extend(["--concurrency", str(concurrency)])
190
+
191
+ if enable_debug:
192
+ coana_args.append("-d")
193
+
194
+ if reach_debug:
195
+ coana_args.append("--debug")
196
+
197
+ if disable_external_tool_checks:
198
+ coana_args.append("--disable-external-tool-checks")
199
+
200
+ if use_only_pregenerated_sboms:
201
+ coana_args.append("--use-only-pregenerated-sboms")
202
+
203
+ if continue_on_analysis_errors:
204
+ coana_args.append("--reach-continue-on-analysis-errors")
205
+
206
+ if continue_on_install_errors:
207
+ coana_args.append("--reach-continue-on-install-errors")
208
+
209
+ if continue_on_missing_lock_files:
210
+ coana_args.append("--reach-continue-on-missing-lock-files")
211
+
212
+ if continue_on_no_source_files:
213
+ coana_args.append("--reach-continue-on-no-source-files")
214
+
215
+ # Add any additional parameters provided by the user
216
+ if additional_params:
217
+ coana_args.extend(additional_params)
218
+
219
+ # Set up environment variables
220
+ env = os.environ.copy()
221
+
222
+ # Required environment variables for Coana CLI
223
+ env["SOCKET_ORG_SLUG"] = org_slug
224
+ env["SOCKET_CLI_API_TOKEN"] = self.api_token
225
+
226
+ # Identify the calling CLI to the coana tool / backend (parity with the Node CLI).
227
+ env["SOCKET_CLI_VERSION"] = __version__
228
+ env["SOCKET_CALLER_USER_AGENT"] = _build_caller_user_agent()
229
+
230
+ # NOTE: no proxy env is set here. coana already reads HTTPS_PROXY/HTTP_PROXY itself, and
231
+ # we pass the full parent env above, so it inherits them. A SOCKET_CLI_API_PROXY override
232
+ # should only be set from an explicit --proxy flag (not yet implemented), since seeding it
233
+ # from HTTPS_PROXY would be a no-op (it's the same value coana already resolves).
234
+
235
+ # Optional environment variables.
236
+ # NOTE: repo/branch are intentionally omitted by the caller (passed as None) when they
237
+ # are the default sentinels, to avoid polluting coana's per-repo/branch cache buckets.
238
+ if repo_name:
239
+ env["SOCKET_REPO_NAME"] = repo_name
240
+
241
+ if branch_name:
242
+ env["SOCKET_BRANCH_NAME"] = branch_name
243
+
244
+ # Set NODE_TLS_REJECT_UNAUTHORIZED=0 if allow_unverified is True
245
+ if allow_unverified:
246
+ env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
247
+
248
+ # Execute coana
249
+ log.info("Running reachability analysis...")
250
+ log.debug(f"Environment: SOCKET_ORG_SLUG={org_slug}, SOCKET_REPO_NAME={repo_name or 'not set'}, SOCKET_BRANCH_NAME={branch_name or 'not set'}")
251
+
252
+ try:
253
+ # Prefer npx (with caching disabled); fall back to `npm install` + `node`
254
+ # if the npx launcher fails before coana starts (parity with the Node CLI).
255
+ returncode = self._spawn_coana(coana_args, version, env, target_directory)
256
+
257
+ if returncode != 0:
258
+ log.error(f"Reachability analysis failed with exit code {returncode}")
259
+ raise Exception(f"Reachability analysis failed with exit code {returncode}")
260
+
261
+ # Extract scan ID from output file
262
+ scan_id = self._extract_scan_id(output_path)
263
+
264
+ log.info(f"Reachability analysis completed successfully")
265
+ if scan_id:
266
+ log.info(f"Scan ID: {scan_id}")
267
+
268
+ return {
269
+ "scan_id": scan_id,
270
+ "report_path": output_path,
271
+ "tar_hash_used": tar_hash
272
+ }
273
+
274
+ except Exception as e:
275
+ log.error(f"Failed to run reachability analysis: {str(e)}")
276
+ raise Exception(f"Failed to run reachability analysis: {str(e)}")
277
+
278
+ @staticmethod
279
+ def _sanitize_coana_env(env: Dict[str, str]) -> Dict[str, str]:
280
+ """Drop npm-injected ``npm_package_*`` vars before spawning coana.
281
+
282
+ npm/pnpm/yarn populate one env var per leaf of the cwd's package.json
283
+ (``npm_package_dependencies_*`` etc.). In large monorepos this can be tens of KB
284
+ and push argv+env past the OS ARG_MAX, making the spawn fail with E2BIG before
285
+ coana even starts. coana doesn't read these, so dropping them is safe; we keep
286
+ ``npm_config_*`` (registry/cache/proxy) untouched. Mirrors the Node CLI.
287
+ """
288
+ return {k: v for k, v in env.items() if not k.startswith("npm_package_")}
289
+
290
+ @staticmethod
291
+ def _npx_launcher_failed_before_coana(returncode: int) -> bool:
292
+ """Heuristic: did npx fail *before* coana started (so retrying is worthwhile)?
293
+
294
+ We stream coana's output (no capture), so we classify by exit code alone, like the
295
+ Node CLI does with inherited stdio: signal kills (negative codes) and codes >= 128
296
+ are conventionally launcher/signal failures -> retry. Small positive codes (1..127)
297
+ are ambiguous (coana's own exit codes are small ints), so we do NOT retry.
298
+ """
299
+ return returncode < 0 or returncode >= 128
300
+
301
+ def _spawn_coana(
302
+ self,
303
+ coana_args: List[str],
304
+ version: Optional[str],
305
+ env: Dict[str, str],
306
+ cwd: str,
307
+ ) -> int:
308
+ """Run coana for the given args, returning the process exit code.
309
+
310
+ We run a pinned, versioned spec via npx and intentionally do NOT ``npm install -g``:
311
+ that would silently auto-update the engine on every run and mutate the user's global
312
+ install. The pinned version rides with the Python CLI release instead (see
313
+ ``DEFAULT_COANA_CLI_VERSION``).
314
+
315
+ Primary path: ``npx --yes --force @coana-tech/cli@<version> ...`` — the exact flags the
316
+ Socket Node CLI passes for coana (``--yes`` skips npx's interactive install prompt so
317
+ CI runs don't hang).
318
+
319
+ Fallback path: if npx is missing, or its launcher dies before coana starts, install
320
+ @coana-tech/cli into a temp dir via ``npm install`` and run it directly via ``node``.
321
+ Toggle with ``SOCKET_CLI_COANA_FORCE_NPM_INSTALL`` (use the fallback as the primary
322
+ path) and ``SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK`` (never fall back).
323
+ """
324
+ effective_version = self._resolve_coana_version(version)
325
+ coana_env = self._sanitize_coana_env(env)
326
+ disable_fallback = bool(os.environ.get("SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK"))
327
+
328
+ if os.environ.get("SOCKET_CLI_COANA_FORCE_NPM_INSTALL"):
329
+ return self._spawn_coana_via_npm_install(coana_args, effective_version, coana_env, cwd)
330
+
331
+ package_spec = f"@coana-tech/cli@{effective_version}"
332
+ npx_cmd = ["npx", "--yes", "--force", package_spec, *coana_args]
333
+ log.debug(f"Reachability command: {' '.join(npx_cmd)}")
334
+ try:
335
+ result = subprocess.run(
336
+ npx_cmd,
337
+ env=coana_env,
338
+ cwd=cwd,
339
+ stdout=sys.stderr, # Send stdout to stderr so the user sees it
340
+ stderr=sys.stderr,
341
+ )
342
+ except FileNotFoundError:
343
+ # npx is not on PATH: the launcher provably never started coana.
344
+ if disable_fallback:
345
+ raise
346
+ log.warning("npx not found on PATH; retrying reachability analysis via `npm install` + `node`.")
347
+ return self._spawn_coana_via_npm_install(coana_args, effective_version, coana_env, cwd)
348
+
349
+ if result.returncode == 0:
350
+ return 0
351
+
352
+ if not disable_fallback and self._npx_launcher_failed_before_coana(result.returncode):
353
+ log.warning(
354
+ f"npx launcher failed (exit {result.returncode}) before coana started; "
355
+ "retrying reachability analysis via `npm install` + `node`."
356
+ )
357
+ return self._spawn_coana_via_npm_install(coana_args, effective_version, coana_env, cwd)
358
+
359
+ return result.returncode
360
+
361
+ def _spawn_coana_via_npm_install(
362
+ self,
363
+ coana_args: List[str],
364
+ version: str,
365
+ env: Dict[str, str],
366
+ cwd: str,
367
+ ) -> int:
368
+ """Fallback launcher: ``npm install`` @coana-tech/cli into a temp dir, run via ``node``.
369
+
370
+ Used when npx is unavailable or its launcher fails before coana boots. Mirrors the
371
+ Node CLI's npm-install fallback. Returns coana's exit code; raises if the install
372
+ itself fails or if ``node`` is unavailable.
373
+ """
374
+ script_path = self._install_coana_to_tmpdir(version, env)
375
+ node_cmd = self._build_coana_node_cmd(script_path, coana_args)
376
+ log.debug(f"Reachability fallback command: {' '.join(node_cmd)}")
377
+ try:
378
+ result = subprocess.run(node_cmd, env=env, cwd=cwd, stdout=sys.stderr, stderr=sys.stderr)
379
+ except FileNotFoundError as e:
380
+ # The fallback exists for broken-launcher environments, but it still needs node.
381
+ raise Exception(
382
+ "`node` was not found on PATH; it is required to run the reachability engine "
383
+ "via the npm-install fallback."
384
+ ) from e
385
+ return result.returncode
386
+
387
+ def _install_coana_to_tmpdir(self, version: str, env: Dict[str, str]) -> str:
388
+ """``npm install`` @coana-tech/cli@<version> into a temp dir; return its executable JS path.
389
+
390
+ Caches the resolved path per version for the process lifetime so repeated fallback
391
+ invocations install only once (mirrors the Node CLI's installCoanaToTmpdir). Raises if
392
+ the install fails.
393
+ """
394
+ cached = _INSTALLED_COANA_SCRIPT_PATHS.get(version)
395
+ if cached and os.path.exists(cached):
396
+ return cached
397
+
398
+ install_dir = tempfile.mkdtemp(prefix="socket-coana-")
399
+ _COANA_INSTALL_DIRS.append(install_dir)
400
+ npm_cmd = [
401
+ "npm", "install",
402
+ "--no-save", "--no-package-lock", "--no-audit", "--no-fund",
403
+ "--prefix", install_dir,
404
+ f"@coana-tech/cli@{version}",
405
+ ]
406
+ log.info("Installing reachability analysis engine via npm fallback...")
407
+ log.debug(f"npm install fallback command: {' '.join(npm_cmd)}")
408
+ install = subprocess.run(npm_cmd, env=env, stdout=sys.stderr, stderr=sys.stderr)
409
+ if install.returncode != 0:
410
+ raise Exception(
411
+ f"npm install fallback for @coana-tech/cli@{version} failed with exit code {install.returncode}"
412
+ )
413
+
414
+ script_path = self._resolve_coana_bin(install_dir)
415
+ _INSTALLED_COANA_SCRIPT_PATHS[version] = script_path
416
+ return script_path
417
+
418
+ @staticmethod
419
+ def _resolve_coana_bin(install_dir: str) -> str:
420
+ """Resolve @coana-tech/cli's executable JS from its installed package.json ``bin`` field."""
421
+ package_json_path = os.path.join(
422
+ install_dir, "node_modules", "@coana-tech", "cli", "package.json"
423
+ )
424
+ with open(package_json_path, "r") as f:
425
+ pkg = json.load(f)
426
+ bin_field = pkg.get("bin")
427
+ relative_bin = None
428
+ if isinstance(bin_field, str):
429
+ relative_bin = bin_field
430
+ elif isinstance(bin_field, dict):
431
+ # Prefer an entry named "coana"; otherwise take the first.
432
+ relative_bin = bin_field.get("coana") or next(iter(bin_field.values()), None)
433
+ if not relative_bin:
434
+ raise Exception(
435
+ f"@coana-tech/cli package.json at {package_json_path} is missing a usable bin entry"
436
+ )
437
+ return os.path.abspath(os.path.join(os.path.dirname(package_json_path), relative_bin))
438
+
439
+ @staticmethod
440
+ def _build_coana_node_cmd(script_path: str, coana_args: List[str]) -> List[str]:
441
+ """Run a .js/.mjs entry via ``node``; invoke a native binary directly (Node CLI parity)."""
442
+ if script_path.endswith(".js") or script_path.endswith(".mjs"):
443
+ return ["node", script_path, *coana_args]
444
+ return [script_path, *coana_args]
445
+
446
+ def _extract_scan_id(self, facts_file_path: str) -> Optional[str]:
447
+ """
448
+ Extract tier1ReachabilityScanId from the socket facts JSON file.
449
+
450
+ Args:
451
+ facts_file_path: Path to the .socket.facts.json file
452
+
453
+ Returns:
454
+ Optional[str]: The scan ID if found, None otherwise
455
+ """
456
+ try:
457
+ if not os.path.exists(facts_file_path):
458
+ log.warning(f"Facts file not found: {facts_file_path}")
459
+ return None
460
+
461
+ with open(facts_file_path, 'r') as f:
462
+ facts = json.load(f)
463
+
464
+ scan_id = facts.get('tier1ReachabilityScanId')
465
+ return scan_id.strip() if scan_id else None
466
+
467
+ except (json.JSONDecodeError, IOError) as e:
468
+ log.warning(f"Failed to extract scan ID from {facts_file_path}: {e}")
469
+ return None
@@ -189,7 +189,7 @@ def main_code():
189
189
  # Check for required dependencies if reachability analysis is enabled
190
190
  if config.reach:
191
191
  log.info("Reachability analysis enabled, checking for required dependencies...")
192
- required_deps = ["npm", "uv", "npx"]
192
+ required_deps = ["npm", "node", "uv", "npx"]
193
193
  missing_deps = []
194
194
  found_deps = []
195
195