socketsecurity 2.4.5__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.
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/CHANGELOG.md +36 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/PKG-INFO +1 -1
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/docs/cli-reference.md +27 -11
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/pyproject.toml +1 -1
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/__init__.py +1 -1
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/config.py +4 -2
- socketsecurity-2.4.7/socketsecurity/core/tools/reachability.py +469 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/socketcli.py +1 -1
- socketsecurity-2.4.7/tests/unit/test_reachability.py +384 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/uv.lock +64 -50
- socketsecurity-2.4.5/socketsecurity/core/tools/reachability.py +0 -330
- socketsecurity-2.4.5/tests/unit/test_reachability.py +0 -106
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/CODEOWNERS +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/actions/setup-docker/action.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/actions/setup-hatch/action.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/actions/setup-sfw/action.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/dependabot.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/workflows/dependency-review.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/workflows/docker-stable.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/workflows/e2e-test.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/workflows/pr-preview.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/workflows/python-tests.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/workflows/release.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/workflows/version-check.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.github/zizmor.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.gitignore +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.hooks/sync_version.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.pre-commit-config.yaml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/.python-version +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/Dockerfile +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/LICENSE +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/Makefile +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/README.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/docs/ci-cd.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/docs/development.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/docs/troubleshooting.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/examples/config/sarif-dashboard-parity.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/examples/config/sarif-dashboard-parity.toml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/examples/config/sarif-diff-ci-cd.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/examples/config/sarif-diff-ci-cd.toml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/examples/config/sarif-instance-detail.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/examples/config/sarif-instance-detail.toml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/instructions/gitlab-commit-status/uat.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/pytest.ini +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/scripts/build_container.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/scripts/build_container_flexible.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/scripts/deploy-test-docker.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/scripts/deploy-test-pypi.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/scripts/docker-entrypoint.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/scripts/run.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/session.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socket.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/__init__.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/alert_selection.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/classes.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/cli_client.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/exceptions.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/git_interface.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/helper/__init__.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/lazy_file_loader.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/logging.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/messages.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/resource_utils.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/scm/__init__.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/scm/base.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/scm/client.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/scm/github.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/scm/gitlab.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/scm_comments.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/socket_config.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/core/utils.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/fossa_compat.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/output.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/__init__.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/base.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/formatters/__init__.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/formatters/slack.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/jira.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/manager.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/slack.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/teams.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/socketsecurity/plugins/webhook.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/__init__.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/conftest.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/create_diff_input.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/test_diff_alerts.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/test_diff_generation.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/test_facts_compression.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/test_has_manifest_files.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/test_package_and_alerts.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/test_sdk_methods.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/core/test_supporting_methods.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/create_response.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/diff/stream_diff.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/head_scan/metadata.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/new_scan/metadata.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/repos/repo_info_error.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/repos/repo_info_no_head.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/repos/repo_info_success.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/data/settings/security-policy.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/fixtures/simple-npm/index.js +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/fixtures/simple-npm/package.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/validate-gitlab.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/validate-json.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/validate-reachability.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/validate-sarif.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/e2e/validate-scan.sh +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/fixtures/fossa/README.md +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-analyze-empty.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-analyze-populated.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-sbom-empty-deep.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/fixtures/fossa/fossa-sbom-populated.json +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/__init__.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_alert_selection.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_cli_config.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_client.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_config.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_dependency_overview.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_disable_ignore.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_exclude_paths.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_fossa_compat.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_fossa_parity.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_gitlab_auth.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_gitlab_auth_fallback.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_gitlab_commit_status.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_gitlab_format.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_ignore_telemetry_filtering.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_output.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_slack_plugin.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_socketcli.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/tests/unit/test_tier1_finalize.py +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/workflows/bitbucket-pipelines.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/workflows/buildkite.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/workflows/github-actions.yml +0 -0
- {socketsecurity-2.4.5 → socketsecurity-2.4.7}/workflows/gitlab-ci.yml +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
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
|
+
|
|
24
|
+
## 2.4.6
|
|
25
|
+
|
|
26
|
+
### Docs: reachability reference corrections
|
|
27
|
+
|
|
28
|
+
- Documented the `uv` and Enterprise-plan prerequisites the CLI enforces **before** running
|
|
29
|
+
reachability (exit code 3 if unmet), and clarified that per-ecosystem build toolchains
|
|
30
|
+
(JDK / .NET / Go / a compatible Python interpreter) are checked by the analysis engine at
|
|
31
|
+
runtime, not pre-checked by the CLI.
|
|
32
|
+
- Corrected the `--reach-min-severity` values to `info, low, moderate, high, critical`.
|
|
33
|
+
- Documented the previously-undocumented reachability flags: `--reach-enable-analysis-splitting`,
|
|
34
|
+
`--reach-detailed-analysis-log-file`, `--reach-lazy-mode`, and `--reach-use-only-pregenerated-sboms`.
|
|
35
|
+
- Clarified that `--only-facts-file` submits only the facts file when **creating** the full scan
|
|
36
|
+
(it does not require a pre-existing scan).
|
|
37
|
+
- Documentation-only; no functional code changes.
|
|
38
|
+
|
|
3
39
|
## 2.4.5
|
|
4
40
|
|
|
5
41
|
### Changed: Bump required SDK version to `>=3.2.1`
|
|
@@ -154,7 +154,8 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [--
|
|
|
154
154
|
[--ignore-commit-files] [--disable-blocking] [--disable-ignore] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders]
|
|
155
155
|
[--reach] [--reach-version REACH_VERSION] [--reach-analysis-timeout REACH_ANALYSIS_TIMEOUT]
|
|
156
156
|
[--reach-analysis-memory-limit REACH_ANALYSIS_MEMORY_LIMIT] [--reach-concurrency REACH_CONCURRENCY] [--reach-ecosystems REACH_ECOSYSTEMS]
|
|
157
|
-
[--reach-min-severity
|
|
157
|
+
[--reach-min-severity <level>] [--reach-skip-cache] [--reach-disable-analytics] [--reach-enable-analysis-splitting] [--reach-detailed-analysis-log-file]
|
|
158
|
+
[--reach-lazy-mode] [--reach-use-only-pregenerated-sboms] [--reach-debug] [--reach-disable-external-tool-checks]
|
|
158
159
|
[--reach-output-file REACH_OUTPUT_FILE] [--only-facts-file] [--version]
|
|
159
160
|
````
|
|
160
161
|
|
|
@@ -238,25 +239,36 @@ If you don't want to provide the Socket API Token every time then you can use th
|
|
|
238
239
|
#### Reachability Analysis
|
|
239
240
|
| Parameter | Required | Default | Description |
|
|
240
241
|
|:---------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------|
|
|
241
|
-
| `--reach` | False | False | Enable reachability analysis to identify which vulnerable functions are actually called by your code
|
|
242
|
-
| `--reach-version` | False |
|
|
243
|
-
| `--reach-analysis-timeout` | False |
|
|
244
|
-
| `--reach-analysis-memory-limit` | False |
|
|
245
|
-
| `--reach-concurrency` | False |
|
|
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 | 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. |
|
|
246
247
|
| `--reach-additional-params` | False | | Pass custom parameters to the coana CLI tool |
|
|
247
248
|
| `--reach-ecosystems` | False | | Comma-separated list of ecosystems to analyze (e.g., "npm,pypi"). If not specified, all supported ecosystems are analyzed |
|
|
248
|
-
| `--reach-min-severity` | False |
|
|
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. |
|
|
249
250
|
| `--reach-skip-cache` | False | False | Skip cache and force fresh reachability analysis |
|
|
250
251
|
| `--reach-disable-analytics` | False | False | Disable analytics collection during reachability analysis |
|
|
252
|
+
| `--reach-enable-analysis-splitting` | False | False | Enable analysis splitting/bucketing (a legacy performance feature). Splitting is disabled by default. |
|
|
253
|
+
| `--reach-detailed-analysis-log-file` | False | False | Write a detailed analysis log file; its path is printed to stdout |
|
|
254
|
+
| `--reach-lazy-mode` | False | False | Enable lazy mode (experimental performance feature) |
|
|
255
|
+
| `--reach-use-only-pregenerated-sboms` | False | False | Build the scan only from pre-generated CycloneDX (CDX) and SPDX files in your project (requires --reach) |
|
|
251
256
|
| `--reach-debug` | False | False | Enable coana debug output (`--debug`) for the analysis, independent of the global `--enable-debug` |
|
|
252
257
|
| `--reach-disable-external-tool-checks` | False | False | Disable coana's external tool availability checks (passes `--disable-external-tool-checks`) |
|
|
253
258
|
| `--reach-output-file` | False | .socket.facts.json | Path where reachability analysis results should be saved |
|
|
254
259
|
| `--reach-exclude-paths` | False | | **[DEPRECATED — use `--exclude-paths`]** Comma-separated paths to exclude from reachability analysis. Still honored (unioned with `--exclude-paths`) but will be hidden in a future release |
|
|
255
|
-
| `--only-facts-file` | False | False | Submit only the .socket.facts.json file
|
|
260
|
+
| `--only-facts-file` | False | False | Submit only the .socket.facts.json file when creating the full scan (requires --reach) |
|
|
256
261
|
|
|
257
262
|
**Reachability Analysis Requirements:**
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
|
|
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 (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)
|
|
268
|
+
- `uv` - Required by the analysis engine
|
|
269
|
+
- An **Enterprise** Socket organization plan (any `enterprise*` plan, including Enterprise trials)
|
|
270
|
+
|
|
271
|
+
Separately, the analysis engine (coana) needs the **per-ecosystem build toolchain** for whatever languages your project uses — e.g. a compatible Python interpreter (3.11+, or PyPy) for Python, a JDK for Java/Kotlin/Scala, .NET 6+ for C#, the matching Go toolchain for Go, etc. These are validated by the engine **at analysis time** (the CLI does not pre-check them) and that validation can be skipped with `--reach-disable-external-tool-checks`.
|
|
260
272
|
|
|
261
273
|
## Config file support
|
|
262
274
|
|
|
@@ -302,7 +314,11 @@ Sample config files:
|
|
|
302
314
|
|
|
303
315
|
For CI-specific examples and guidance, see [`ci-cd.md`](ci-cd.md).
|
|
304
316
|
|
|
305
|
-
The CLI
|
|
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.
|
|
306
322
|
|
|
307
323
|
#### Advanced Configuration
|
|
308
324
|
| Parameter | Required | Default | Description |
|
|
@@ -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="
|
|
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
|
|