Skip to content

Commit adb2de7

Browse files
committed
Pin @coana-tech/cli version; make reachability auto-update opt-in
The Python CLI auto-updated the reachability (Coana) engine to the latest published version on every --reach run via `npm install -g @coana-tech/cli`. Automatically pulling a brand-new engine version without opting in is undesirable for environments that need to review/approve dependency updates before adopting them. Run a fixed, pinned version (DEFAULT_COANA_CLI_VERSION = 15.3.22) via `npx @coana-tech/cli@<pinned>` instead, so the engine version only changes through a standard pip upgrade of this CLI. Opt into newest with `--reach-version latest`; pin an explicit version with `--reach-version <semver>`. The global `npm install -g` step is dropped entirely, so an existing global install is never auto-updated or downgraded.
1 parent 405fdc9 commit adb2de7

8 files changed

Lines changed: 121 additions & 75 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

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.22`) 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+
316
## 2.4.6
417

518
### Docs: reachability reference corrections

docs/cli-reference.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ If you don't want to provide the Socket API Token every time then you can use th
240240
| Parameter | Required | Default | Description |
241241
|:---------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------|
242242
| `--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 |
243+
| `--reach-version` | False | *pinned* | Version of @coana-tech/cli to use. Defaults to the version pinned to this CLI release (currently `15.3.22`), 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. |
244244
| `--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` |
245245
| `--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` |
246246
| `--reach-concurrency` | False | *coana* | Control parallel analysis execution (must be >= 1). Omitted by default, so coana applies its own (currently 1) |
@@ -262,8 +262,8 @@ If you don't want to provide the Socket API Token every time then you can use th
262262
**Reachability Analysis Requirements:**
263263
264264
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)
267267
- `uv` - Required by the analysis engine
268268
- An **Enterprise** Socket organization plan (any `enterprise*` plan, including Enterprise trials)
269269
@@ -313,7 +313,7 @@ Sample config files:
313313
314314
For CI-specific examples and guidance, see [`ci-cd.md`](ci-cd.md).
315315
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.
316+
The CLI runs a pinned `@coana-tech/cli` version via `npx` (fetched on first use, then cached); it does **not** auto-update the engine or install it globally. 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.
317317
318318
#### Advanced Configuration
319319
| Parameter | Required | Default | Description |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.4.6"
9+
version = "2.4.7"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.4.6'
2+
__version__ = '2.4.7'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

socketsecurity/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -943,8 +943,10 @@ def create_argument_parser() -> argparse.ArgumentParser:
943943
reachability_group.add_argument(
944944
"--reach-version",
945945
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."
948950
)
949951
reachability_group.add_argument(
950952
"--reach-analysis-timeout",

socketsecurity/core/tools/reachability.py

Lines changed: 28 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212

1313
log = logging.getLogger(__name__)
1414

15+
# Pinned @coana-tech/cli version. Bumped deliberately per Python CLI release so the
16+
# reachability engine version only changes through a standard pip upgrade (advance notice).
17+
# Pass --reach-version latest to opt into the newest published version instead.
18+
DEFAULT_COANA_CLI_VERSION = "15.3.22"
19+
1520

1621
def _build_caller_user_agent() -> str:
1722
"""Build the SOCKET_CALLER_USER_AGENT string forwarded to the coana CLI.
@@ -31,70 +36,28 @@ def __init__(self, sdk: socketdev, api_token: str):
3136
self.sdk = sdk
3237
self.api_token = api_token
3338

34-
def _ensure_coana_cli_installed(self, version: Optional[str] = None) -> str:
39+
def _resolve_coana_package_spec(self, version: Optional[str] = None) -> str:
3540
"""
36-
Check if @coana-tech/cli is installed, and install/update it if needed.
37-
41+
Resolve the @coana-tech/cli package spec to run with npx.
42+
43+
We pass an exact, versioned spec to npx so it runs a deterministic version from its
44+
own cache (fetching once if absent). We intentionally do NOT ``npm install -g`` here:
45+
that would silently auto-update the engine on every run and mutate the user's global
46+
install. The pinned version rides with the Python CLI release instead, so engine
47+
changes only happen through a standard pip upgrade (advance notice).
48+
3849
Args:
39-
version: Specific version to install (e.g., '1.2.3'). If None, always updates to latest.
40-
50+
version: Coana CLI version to use.
51+
- None: the pinned ``DEFAULT_COANA_CLI_VERSION`` (no auto-update).
52+
- 'latest': always the newest published version (opt-in to auto-update).
53+
- '<semver>': that exact version.
54+
4155
Returns:
42-
str: The package specifier to use with npx
56+
str: The package specifier to use with npx (e.g. '@coana-tech/cli@15.3.22').
4357
"""
44-
# Determine the package specifier
45-
package_spec = f"@coana-tech/cli@{version}" if version else "@coana-tech/cli"
46-
47-
# If a specific version is requested, check if it's already installed
48-
if version:
49-
try:
50-
check_cmd = ["npm", "list", "-g", "@coana-tech/cli", "--depth=0"]
51-
result = subprocess.run(
52-
check_cmd,
53-
capture_output=True,
54-
text=True,
55-
timeout=10
56-
)
57-
58-
# If npm list succeeds and mentions the specific version, it's installed
59-
if result.returncode == 0 and f"@coana-tech/cli@{version}" in result.stdout:
60-
log.debug(f"@coana-tech/cli@{version} is already installed globally")
61-
return package_spec
62-
63-
except Exception as e:
64-
log.debug(f"Could not check for existing @coana-tech/cli installation: {e}")
65-
66-
# Install or update the package
67-
# When no version is specified, always try to update to latest
68-
if version:
69-
log.info(f"Installing reachability analysis plugin (@coana-tech/cli@{version})...")
70-
else:
71-
log.info("Updating reachability analysis plugin (@coana-tech/cli) to latest version...")
72-
log.info("This may take a moment...")
73-
74-
try:
75-
install_cmd = ["npm", "install", "-g", package_spec]
76-
log.debug(f"Installing with command: {' '.join(install_cmd)}")
77-
78-
result = subprocess.run(
79-
install_cmd,
80-
capture_output=True,
81-
text=True,
82-
timeout=300 # 5 minute timeout for installation
83-
)
84-
85-
if result.returncode != 0:
86-
log.warning(f"Global installation failed, npx will download on demand")
87-
log.debug(f"Install stderr: {result.stderr}")
88-
else:
89-
log.info("Reachability analysis plugin installed successfully")
90-
91-
except subprocess.TimeoutExpired:
92-
log.warning("Installation timed out, npx will download on demand")
93-
except Exception as e:
94-
log.warning(f"Could not install globally: {e}, npx will download on demand")
95-
96-
return package_spec
97-
58+
effective = (version or DEFAULT_COANA_CLI_VERSION).strip()
59+
return f"@coana-tech/cli@{effective}"
60+
9861

9962
def run_reachability_analysis(
10063
self,
@@ -147,7 +110,9 @@ def run_reachability_analysis(
147110
lazy_mode: Enable lazy mode for analysis
148111
repo_name: Repository name
149112
branch_name: Branch name
150-
version: Specific version of @coana-tech/cli to use
113+
version: @coana-tech/cli version to use. None uses the pinned
114+
DEFAULT_COANA_CLI_VERSION (no auto-update); 'latest' opts into the newest
115+
published version; '<semver>' pins an explicit version.
151116
concurrency: Concurrency level for analysis (must be >= 1)
152117
additional_params: Additional parameters to pass to coana CLI
153118
allow_unverified: Disable SSL certificate verification (sets NODE_TLS_REJECT_UNAUTHORIZED=0)
@@ -157,8 +122,8 @@ def run_reachability_analysis(
157122
Returns:
158123
Dict containing scan_id and report_path
159124
"""
160-
# Ensure @coana-tech/cli is installed
161-
cli_package = self._ensure_coana_cli_installed(version)
125+
# Resolve the pinned (or explicitly requested) @coana-tech/cli version for npx
126+
cli_package = self._resolve_coana_package_spec(version)
162127

163128
# Build CLI command arguments
164129
cmd = ["npx", cli_package, "run", "."]

tests/unit/test_reachability.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
These cover the arg-builder and environment wiring in
44
``socketsecurity.core.tools.reachability.ReachabilityAnalyzer`` without actually
5-
invoking npm/npx/coana: ``_ensure_coana_cli_installed`` and ``subprocess.run`` are mocked.
5+
invoking npx/coana: ``_resolve_coana_package_spec`` and ``subprocess.run`` are mocked.
66
"""
77
from unittest.mock import MagicMock
88

@@ -11,6 +11,7 @@
1111
from socketsecurity import __version__
1212
from socketsecurity.core.tools import reachability
1313
from socketsecurity.core.tools.reachability import (
14+
DEFAULT_COANA_CLI_VERSION,
1415
ReachabilityAnalyzer,
1516
_build_caller_user_agent,
1617
)
@@ -22,8 +23,12 @@ def analyzer():
2223

2324

2425
def _run(analyzer, mocker, **kwargs):
25-
"""Invoke run_reachability_analysis with npm/npx/coana mocked; return (cmd, env)."""
26-
mocker.patch.object(analyzer, "_ensure_coana_cli_installed", return_value="@coana-tech/cli")
26+
"""Invoke run_reachability_analysis with the spec resolver/coana mocked; return (cmd, env)."""
27+
mocker.patch.object(
28+
analyzer,
29+
"_resolve_coana_package_spec",
30+
return_value=f"@coana-tech/cli@{DEFAULT_COANA_CLI_VERSION}",
31+
)
2732
mocker.patch.object(analyzer, "_extract_scan_id", return_value="scan-123")
2833
completed = MagicMock()
2934
completed.returncode = 0
@@ -104,3 +109,64 @@ def test_repo_branch_env_absent_when_none(analyzer, mocker):
104109
_, env = _run(analyzer, mocker, repo_name=None, branch_name=None)
105110
assert "SOCKET_REPO_NAME" not in env
106111
assert "SOCKET_BRANCH_NAME" not in env
112+
113+
114+
# --- Coana package-spec resolution (pinned by default, latest is opt-in) ---
115+
116+
117+
def test_resolve_spec_defaults_to_pinned_version(analyzer):
118+
"""No --reach-version -> pinned DEFAULT_COANA_CLI_VERSION (no auto-update)."""
119+
assert (
120+
analyzer._resolve_coana_package_spec(None)
121+
== f"@coana-tech/cli@{DEFAULT_COANA_CLI_VERSION}"
122+
)
123+
124+
125+
def test_resolve_spec_pins_explicit_version(analyzer):
126+
assert analyzer._resolve_coana_package_spec("1.2.3") == "@coana-tech/cli@1.2.3"
127+
128+
129+
def test_resolve_spec_latest_opt_in(analyzer):
130+
"""'latest' opts into the newest published version."""
131+
assert analyzer._resolve_coana_package_spec("latest") == "@coana-tech/cli@latest"
132+
133+
134+
def test_resolve_spec_is_always_versioned(analyzer):
135+
"""Never the bare '@coana-tech/cli' (which would let npx pick a stray global version)."""
136+
for version in (None, "latest", "1.2.3", " 1.2.3 "):
137+
assert analyzer._resolve_coana_package_spec(version).startswith("@coana-tech/cli@")
138+
139+
140+
def _run_with_real_resolver(analyzer, mocker, **kwargs):
141+
"""Like _run but exercises the real _resolve_coana_package_spec; returns the run mock."""
142+
mocker.patch.object(analyzer, "_extract_scan_id", return_value="scan-123")
143+
completed = MagicMock()
144+
completed.returncode = 0
145+
run_mock = mocker.patch.object(reachability.subprocess, "run", return_value=completed)
146+
analyzer.run_reachability_analysis(org_slug="my-org", target_directory=".", **kwargs)
147+
return run_mock
148+
149+
150+
def test_npx_runs_pinned_version_by_default(analyzer, mocker):
151+
run_mock = _run_with_real_resolver(analyzer, mocker)
152+
cmd = run_mock.call_args.args[0]
153+
assert cmd[0] == "npx"
154+
assert cmd[1] == f"@coana-tech/cli@{DEFAULT_COANA_CLI_VERSION}"
155+
156+
157+
def test_npx_runs_explicit_version(analyzer, mocker):
158+
run_mock = _run_with_real_resolver(analyzer, mocker, version="9.9.9")
159+
assert run_mock.call_args.args[0][1] == "@coana-tech/cli@9.9.9"
160+
161+
162+
def test_npx_runs_latest_when_opted_in(analyzer, mocker):
163+
run_mock = _run_with_real_resolver(analyzer, mocker, version="latest")
164+
assert run_mock.call_args.args[0][1] == "@coana-tech/cli@latest"
165+
166+
167+
def test_never_runs_npm_install(analyzer, mocker):
168+
"""Core guarantee: we never `npm install -g` (no auto-update / global mutation)."""
169+
run_mock = _run_with_real_resolver(analyzer, mocker)
170+
for call in run_mock.call_args_list:
171+
argv = call.args[0]
172+
assert argv[:2] != ["npm", "install"]

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)