Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 2.4.9

### Changed: consolidated coana launcher env vars into `SOCKET_CLI_COANA_LAUNCHER`

- The reachability launcher is now tuned via a single `SOCKET_CLI_COANA_LAUNCHER`
environment variable (mirroring the Socket Node CLI): `auto` (default when unset; try
`npx` first, fall back to `npm install` + `node` on launcher-level failures),
`npm-install` (skip `npx` entirely), or `npx` (never fall back). An unrecognized value
logs a warning and behaves as `auto`.
- The legacy `SOCKET_CLI_COANA_FORCE_NPM_INSTALL` and `SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK`
variables remain supported for back-compat when `SOCKET_CLI_COANA_LAUNCHER` is unset, but
are deprecated and no longer documented.

## 2.4.8

### Fixed: retry transient full-scan upload failures
Expand Down
7 changes: 4 additions & 3 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,10 @@ For CI-specific examples and guidance, see [`ci-cd.md`](ci-cd.md).

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.

The launcher fallback can be tuned via environment variables:
- `SOCKET_CLI_COANA_FORCE_NPM_INSTALL` — skip `npx` entirely and always use the `npm install` + `node` path (useful where `npx` is known-broken).
- `SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK` — never fall back; surface the `npx` failure directly.
The launcher can be tuned via the `SOCKET_CLI_COANA_LAUNCHER` environment variable:
- `auto` (default when unset) — try `npx` first; fall back to `npm install` + `node` if the launcher fails before the engine starts.
- `npm-install` — skip `npx` entirely and always use the `npm install` + `node` path (useful where `npx` is known-broken).
- `npx` — never fall back; surface the `npx` failure directly.

#### Advanced Configuration
| Parameter | Required | Default | Description |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"

[project]
name = "socketsecurity"
version = "2.4.8"
version = "2.4.9"
requires-python = ">= 3.11"
license = {"file" = "LICENSE"}
dependencies = [
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__author__ = 'socket.dev'
__version__ = '2.4.8'
__version__ = '2.4.9'
USER_AGENT = f'SocketPythonCLI/{__version__}'
38 changes: 32 additions & 6 deletions socketsecurity/core/tools/reachability.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,31 @@ def _npx_launcher_failed_before_coana(returncode: int) -> bool:
"""
return returncode < 0 or returncode >= 128

@staticmethod
def _resolve_coana_launcher_mode() -> str:
"""Resolve the coana launcher mode: ``auto``, ``npx``, or ``npm-install``.

``SOCKET_CLI_COANA_LAUNCHER`` wins when set to a recognized value; an unrecognized
value warns and behaves as ``auto``. Only when it is unset/empty do the legacy vars
apply: ``SOCKET_CLI_COANA_FORCE_NPM_INSTALL`` -> ``npm-install``, else
``SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK`` -> ``npx``. Mirrors the Node CLI.
"""
raw = os.environ.get("SOCKET_CLI_COANA_LAUNCHER", "")
mode = raw.strip().lower()
if mode in ("auto", "npx", "npm-install"):
return mode
if mode:
log.warning(
f'Ignoring unrecognized SOCKET_CLI_COANA_LAUNCHER value "{raw}"; '
'expected "auto", "npm-install", or "npx".'
)
return "auto"
if os.environ.get("SOCKET_CLI_COANA_FORCE_NPM_INSTALL"):
return "npm-install"
if os.environ.get("SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK"):
return "npx"
return "auto"

def _spawn_coana(
self,
coana_args: List[str],
Expand All @@ -318,14 +343,15 @@ def _spawn_coana(

Fallback path: if npx is missing, or its launcher dies before coana starts, install
@coana-tech/cli into a temp dir via ``npm install`` and run it directly via ``node``.
Toggle with ``SOCKET_CLI_COANA_FORCE_NPM_INSTALL`` (use the fallback as the primary
path) and ``SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK`` (never fall back).
Tune with ``SOCKET_CLI_COANA_LAUNCHER``: ``auto`` (default; npx with the npm-install
fallback), ``npm-install`` (skip npx, always use the fallback path), or ``npx``
(never fall back).
"""
effective_version = self._resolve_coana_version(version)
coana_env = self._sanitize_coana_env(env)
disable_fallback = bool(os.environ.get("SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK"))
launcher_mode = self._resolve_coana_launcher_mode()

if os.environ.get("SOCKET_CLI_COANA_FORCE_NPM_INSTALL"):
if launcher_mode == "npm-install":
return self._spawn_coana_via_npm_install(coana_args, effective_version, coana_env, cwd)

package_spec = f"@coana-tech/cli@{effective_version}"
Expand All @@ -341,15 +367,15 @@ def _spawn_coana(
)
except FileNotFoundError:
# npx is not on PATH: the launcher provably never started coana.
if disable_fallback:
if launcher_mode == "npx":
raise
log.warning("npx not found on PATH; retrying reachability analysis via `npm install` + `node`.")
return self._spawn_coana_via_npm_install(coana_args, effective_version, coana_env, cwd)

if result.returncode == 0:
return 0

if not disable_fallback and self._npx_launcher_failed_before_coana(result.returncode):
if launcher_mode != "npx" and self._npx_launcher_failed_before_coana(result.returncode):
log.warning(
f"npx launcher failed (exit {result.returncode}) before coana started; "
"retrying reachability analysis via `npm install` + `node`."
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_reachability.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,54 @@ def fake_run(argv, **_kw):
assert all(c[:2] != ["npm", "install"] for c in calls)


def test_launcher_npm_install_skips_npx(analyzer, mocker, monkeypatch):
"""SOCKET_CLI_COANA_LAUNCHER=npm-install routes straight to npm install + node."""
monkeypatch.setenv("SOCKET_CLI_COANA_LAUNCHER", "npm-install")
calls = _capture_spawns(analyzer, mocker, npx_behavior=0)
assert all(c[0] != "npx" for c in calls)
assert calls[0][:2] == ["npm", "install"]
assert calls[1][0] == "node"


def test_launcher_npx_propagates_npx_failure(analyzer, mocker, monkeypatch):
"""SOCKET_CLI_COANA_LAUNCHER=npx: a launcher failure is NOT retried via npm."""
monkeypatch.setenv("SOCKET_CLI_COANA_LAUNCHER", "npx")
mocker.patch.object(analyzer, "_extract_scan_id", return_value=None)
calls = []

def fake_run(argv, **_kw):
calls.append(argv)
m = MagicMock()
m.returncode = 137
return m

mocker.patch.object(reachability.subprocess, "run", side_effect=fake_run)
with pytest.raises(Exception):
analyzer.run_reachability_analysis(org_slug="my-org", target_directory=".")
assert calls[0][0] == "npx"
assert all(c[:2] != ["npm", "install"] for c in calls)


def test_launcher_overrides_legacy_vars(analyzer, mocker, monkeypatch):
"""A recognized SOCKET_CLI_COANA_LAUNCHER wins; legacy vars are ignored entirely."""
monkeypatch.setenv("SOCKET_CLI_COANA_LAUNCHER", "auto")
monkeypatch.setenv("SOCKET_CLI_COANA_FORCE_NPM_INSTALL", "1")
monkeypatch.setenv("SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK", "1")
calls = _capture_spawns(analyzer, mocker, npx_behavior=137)
assert calls[0][0] == "npx" # force-npm-install ignored: npx still attempted
assert calls[1][:2] == ["npm", "install"] # disable-fallback ignored: fallback runs
assert calls[2][0] == "node"


def test_launcher_unrecognized_value_behaves_as_auto(analyzer, mocker, monkeypatch):
"""An unrecognized SOCKET_CLI_COANA_LAUNCHER value warns and behaves as auto."""
monkeypatch.setenv("SOCKET_CLI_COANA_LAUNCHER", "bogus")
calls = _capture_spawns(analyzer, mocker, npx_behavior=137)
assert calls[0][0] == "npx"
assert calls[1][:2] == ["npm", "install"]
assert calls[2][0] == "node"


def test_fallback_installs_once_per_version(analyzer, mocker):
"""A second in-process fallback for the same version reuses the install (no re-install)."""
mocker.patch.object(analyzer, "_extract_scan_id", return_value="scan-123")
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading