Skip to content

Commit 3e4c988

Browse files
authored
Merge branch 'main' into dependabot/uv/python-minor-patch-50bad41824
2 parents 10f4f0b + 52178cc commit 3e4c988

14 files changed

Lines changed: 547 additions & 631 deletions

File tree

‎.github/workflows/python-tests.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
branches: [main]
99
paths:
1010
- "socketsecurity/**/*.py"
11+
- "socketsecurity/**/*.mjs"
1112
- "tests/unit/**/*.py"
1213
- "tests/core/**/*.py"
1314
- "pyproject.toml"

‎CHANGELOG.md‎

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

3+
## 2.11.1
4+
5+
### Changed: bump pinned Coana CLI to 15.12.10
6+
7+
- Bumped the pinned reachability engine (the Coana CLI) from `15.12.8` to
8+
`15.12.10`. See the
9+
[reachability analysis changelog](https://docs.socket.dev/docs/reachability-analysis-changelog)
10+
for engine changes.
11+
12+
## 2.11.0
13+
14+
### Changed: download the Coana CLI from Socket instead of npm
15+
16+
- The Coana CLI is now downloaded from Socket's servers instead of npm, and is now cached in
17+
`$XDG_CACHE_HOME/socket/coana` (`~/.cache/socket/coana` by default).
18+
- The `SOCKET_CLI_COANA_LAUNCHER` environment variable no longer has an effect.
19+
20+
## 2.10.12
21+
22+
### Changed: bump pinned @coana-tech/cli to 15.12.8
23+
24+
- Bumped the pinned reachability engine (`@coana-tech/cli`) from `15.12.7` to
25+
`15.12.8`. See the
26+
[reachability analysis changelog](https://docs.socket.dev/docs/reachability-analysis-changelog)
27+
for engine changes.
28+
329
## 2.10.11
430

531
### Changed: bump pinned @coana-tech/cli to 15.12.7

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \
7575
fi
7676

7777
# Install additional tools
78-
RUN npm install @coana-tech/cli socket -g && \
78+
RUN npm install socket -g && \
7979
gem install bundler && \
8080
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \
8181
. ~/.cargo/env && \

‎docs/cli-reference.md‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ If you don't want to provide the Socket API Token every time then you can use th
338338
| Parameter | Required | Default | Description |
339339
|:---------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------|
340340
| `--reach` | False | False | Enable reachability analysis to identify which vulnerable functions are actually called by your code. Creates a full application reachability scan (`scan_type=socket_tier1`). |
341-
| `--reach-version` | False | 15.12.7 | 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. |
341+
| `--reach-version` | False | 15.12.10 | Version of the Coana 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. |
342342
| `--reach-analysis-timeout` | False | 10m | Timeout for each reachability analysis run, e.g. `90s`, `10m` or `1h`. Omitted by default, so coana applies its own default (`10m`). Alias: `--reach-timeout` |
343343
| `--reach-analysis-memory-limit` | False | 8GB | Memory limit for each reachability analysis run, e.g. `512MB` or `8GB`. Omitted by default, so coana applies its own default (`8GB`). Alias: `--reach-memory-limit` |
344344
| `--reach-concurrency` | False | 1 | Control parallel analysis execution (must be >= 1). Omitted by default, so coana applies its own default. |
@@ -359,9 +359,7 @@ If you don't want to provide the Socket API Token every time then you can use th
359359
**Reachability Analysis Requirements:**
360360
361361
The Python CLI verifies the following **up front** (before invoking the analysis engine) and exits with code **3** if any are unmet:
362-
- `npm` - Required (verified up front; ships alongside `npx`)
363-
- `npx` - Required to fetch (on first use) and run `@coana-tech/cli` (the analysis engine)
364-
- `node` - Required to run the engine (used directly by the `npm install` fallback)
362+
- `node` - Required to run the analysis engine (the Coana CLI). The engine needs Node.js 20 or later.
365363
- `uv` - Required by the analysis engine
366364
- An **Enterprise** Socket organization plan (any `enterprise*` plan, including Enterprise trials)
367365
@@ -419,12 +417,7 @@ Sample config files:
419417
420418
For CI-specific examples and guidance, see [`ci-cd.md`](ci-cd.md).
421419
422-
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.
423-
424-
The launcher can be tuned via the `SOCKET_CLI_COANA_LAUNCHER` environment variable:
425-
- `auto` (default when unset) — try `npx` first; fall back to `npm install` + `node` if the launcher fails before the engine starts.
426-
- `npm-install` — skip `npx` entirely and always use the `npm install` + `node` path (useful where `npx` is known-broken).
427-
- `npx` — never fall back; surface the `npx` failure directly.
420+
The CLI runs a pinned version of the Coana CLI, the analysis engine; it does **not** auto-update the engine. It downloads the engine from the Socket API on first use and keeps it in `$XDG_CACHE_HOME/socket/coana/v1` (`~/.cache/socket/coana/v1` when `XDG_CACHE_HOME` is unset). 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.
428421
429422
#### Advanced Configuration
430423
| 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.10.11"
9+
version = "2.11.1"
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.10.11'
2+
__version__ = '2.11.1'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

‎socketsecurity/config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
10691069
"--reach-version",
10701070
dest="reach_version",
10711071
metavar="<version|latest>",
1072-
help="Version of @coana-tech/cli to use. Defaults to the version pinned to this CLI "
1072+
help="Version of the Coana CLI to use. Defaults to the version pinned to this CLI "
10731073
"release; pass 'latest' to always use the newest published version (opt-in "
10741074
"auto-update), or an explicit version (e.g. '1.2.3') to pin it."
10751075
)

‎socketsecurity/core/cli_client.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,26 @@ def request(
5555
return response
5656

5757
except requests.exceptions.RequestException as e:
58-
logger.error(f"API request failed: {str(e)}")
58+
server_message = self._server_error_message(e.response)
59+
detail = f"{e}: {server_message}" if server_message else str(e)
60+
logger.error(f"API request failed: {detail}")
5961
# Carry the status forward. Callers that need to react to a specific
6062
# code -- the GitLab auth fallback to the other token scheme, and
6163
# APIFailure.is_transient_error -- have no other way to recover it
6264
# once the requests exception has been translated.
6365
status_code = e.response.status_code if e.response is not None else None
64-
raise APIFailure(f"Request failed: {str(e)}", status_code=status_code) from e
66+
raise APIFailure(f"Request failed: {detail}", status_code=status_code) from e
67+
68+
@staticmethod
69+
def _server_error_message(response: Optional[requests.Response]) -> Optional[str]:
70+
"""The explanation the Socket API gives in its ``{"error": {"message": ...}}`` body."""
71+
if response is None:
72+
return None
73+
try:
74+
message = response.json()["error"]["message"]
75+
except (ValueError, KeyError, TypeError):
76+
return None
77+
return message if isinstance(message, str) and message else None
6578

6679
def post_telemetry_events(self, org_slug: str, events: List[Dict]) -> None:
6780
"""Post telemetry events one at a time to the v0 telemetry API. Fire-and-forget — logs errors but never raises."""
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Generated from coana-package-manager's packages/coana-cache. Don't edit it here.
2+
// See "Launching Coana" in packages/cli/docs/gcs-releases.md there.
3+
4+
// src/cli.ts
5+
import { cp, readdir as readdir2, rename as rename2, rm as rm3 } from "node:fs/promises";
6+
import { join as join3 } from "node:path";
7+
8+
// src/cache.ts
9+
import { mkdir as mkdir2, open, readdir, readFile, rm as rm2, stat, utimes } from "node:fs/promises";
10+
import { homedir } from "node:os";
11+
import { dirname as dirname2, isAbsolute, join as join2, relative } from "node:path";
12+
13+
// ../utils/src/atomic-fs.ts
14+
import { randomUUID } from "node:crypto";
15+
import { existsSync } from "node:fs";
16+
import { chmod, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
17+
import { basename, dirname, join } from "node:path";
18+
async function populateDirAtomically(targetDir, populate) {
19+
if (existsSync(targetDir)) return;
20+
const parentDir = dirname(targetDir);
21+
await mkdir(parentDir, { recursive: true });
22+
const tmpDir = await mkdtemp(join(parentDir, `.${basename(targetDir)}-`));
23+
try {
24+
await populate(tmpDir);
25+
await chmod(tmpDir, 493);
26+
await rename(tmpDir, targetDir).catch((e) => {
27+
if (!existsSync(targetDir)) throw e;
28+
});
29+
} finally {
30+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {
31+
});
32+
}
33+
}
34+
async function writeFileAtomically(path, data) {
35+
const tmpPath = `${path}.${randomUUID()}.tmp`;
36+
try {
37+
await writeFile(tmpPath, data);
38+
await rename(tmpPath, path);
39+
} finally {
40+
await rm(tmpPath, { force: true }).catch(() => {
41+
});
42+
}
43+
}
44+
45+
// src/cache.ts
46+
var SHA256 = /^[0-9a-f]{64}$/;
47+
var VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]*$/;
48+
var COMPONENT = /^[A-Za-z][0-9A-Za-z-]*$/;
49+
var TEMP_DIR = /^\.[0-9a-f]{64}-/;
50+
var ABANDONED_TEMP_DIR_MS = 24 * 60 * 6e4;
51+
function cacheRoot() {
52+
const xdgCacheHome = process.env.XDG_CACHE_HOME;
53+
const cacheHome = xdgCacheHome && isAbsolute(xdgCacheHome) ? xdgCacheHome : join2(homedir(), ".cache");
54+
return join2(cacheHome, "socket", "coana", "v1");
55+
}
56+
var OUT_OF_SPACE_CODES = /* @__PURE__ */ new Set(["ENOSPC", "EDQUOT"]);
57+
var NOT_WRITABLE_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EROFS", ...OUT_OF_SPACE_CODES]);
58+
var CacheNotWritable = class _CacheNotWritable extends Error {
59+
constructor(outOfSpace, cause) {
60+
super(
61+
outOfSpace ? `The disk holding the Coana cache in ${cacheRoot()} is out of space (${cause.message}). Free some up, or set XDG_CACHE_HOME to a directory on another disk.` : `Could not write to the Coana cache in ${cacheRoot()} (${cause.message}). Set XDG_CACHE_HOME to a writable directory.`,
62+
{ cause }
63+
);
64+
}
65+
/** `e` as a {@link CacheNotWritable} when it is such a failure of a path in the cache. */
66+
static from(e) {
67+
if (e instanceof _CacheNotWritable) return e;
68+
if (!(e instanceof Error)) return void 0;
69+
const { code = "", path: source, dest } = e;
70+
if (!NOT_WRITABLE_CODES.has(code)) return void 0;
71+
const outOfSpace = OUT_OF_SPACE_CODES.has(code);
72+
const paths = (outOfSpace && dest !== void 0 ? [dest] : [source, dest]).filter((p) => p !== void 0);
73+
const ofCache = paths.length === 0 ? outOfSpace : paths.some((path) => within(cacheRoot(), path) || within(path, cacheRoot()));
74+
return ofCache ? new _CacheNotWritable(outOfSpace, e) : void 0;
75+
function within(dir, path) {
76+
const fromDir = relative(dir, path);
77+
return !fromDir.startsWith("..") && !isAbsolute(fromDir);
78+
}
79+
}
80+
};
81+
var InvalidCacheKey = class extends Error {
82+
};
83+
async function writing(f) {
84+
try {
85+
return await f();
86+
} catch (e) {
87+
throw CacheNotWritable.from(e) ?? e;
88+
}
89+
}
90+
function objectDir(sha256) {
91+
return join2(cacheRoot(), "objects", sha256);
92+
}
93+
function versionSegment(version) {
94+
if (!VERSION.test(version)) throw new InvalidCacheKey(`Invalid Coana version: ${version}`);
95+
return version;
96+
}
97+
function indexFile(version, component) {
98+
if (!COMPONENT.test(component)) throw new InvalidCacheKey(`Invalid component name: ${component}`);
99+
return join2(cacheRoot(), "versions", versionSegment(version), component);
100+
}
101+
async function lookup(version, component) {
102+
const index = indexFile(version, component);
103+
const sha256 = (await readFile(index, "utf8").catch(() => "")).trim();
104+
if (!SHA256.test(sha256)) return void 0;
105+
const dir = objectDir(sha256);
106+
const isDirectory = await stat(dir).then(
107+
(s) => s.isDirectory(),
108+
() => false
109+
);
110+
if (!isDirectory) return void 0;
111+
const now = /* @__PURE__ */ new Date();
112+
await utimes(index, now, now).catch(() => writeFileAtomically(index, sha256)).catch(() => {
113+
});
114+
return dir;
115+
}
116+
async function install(version, component, sha256, populate) {
117+
if (!SHA256.test(sha256)) throw new InvalidCacheKey(`Invalid SHA-256: ${sha256}`);
118+
const index = indexFile(version, component);
119+
const target = objectDir(sha256);
120+
await writing(() => mkdir2(dirname2(index), { recursive: true }));
121+
await writing(
122+
() => populateDirAtomically(target, (dir) => {
123+
void sweepAbandonedTempDirs();
124+
return populate(dir);
125+
})
126+
);
127+
await writing(() => writeFileAtomically(index, sha256));
128+
return target;
129+
}
130+
var sweeps = /* @__PURE__ */ new Map();
131+
function sweepAbandonedTempDirs() {
132+
const objects = join2(cacheRoot(), "objects");
133+
let sweep = sweeps.get(objects);
134+
if (sweep === void 0) {
135+
sweep = removeAbandoned();
136+
sweeps.set(objects, sweep);
137+
}
138+
return sweep;
139+
async function removeAbandoned() {
140+
const names = await readdir(objects).catch(() => []);
141+
await Promise.all(
142+
names.filter((name) => TEMP_DIR.test(name)).map(async (name) => {
143+
const dir = join2(objects, name);
144+
const age = await ageMs(dir);
145+
if (age !== void 0 && age > ABANDONED_TEMP_DIR_MS)
146+
await rm2(dir, { recursive: true, force: true }).catch(() => {
147+
});
148+
})
149+
);
150+
}
151+
}
152+
async function ageMs(path) {
153+
return stat(path).then(
154+
(s) => Date.now() - s.ctimeMs,
155+
() => void 0
156+
);
157+
}
158+
159+
// src/cli.ts
160+
var USAGE = `usage: coana-cache.mjs lookup <version> <component>
161+
coana-cache.mjs install <version> <component> <sha256> <extracted-dir>`;
162+
var NOT_CACHED = 4;
163+
async function main(args) {
164+
const [command, ...rest] = args;
165+
if (command === "lookup" && rest.length === 2) {
166+
const [version, component] = rest;
167+
const dir = await lookup(version, component);
168+
if (dir === void 0) return NOT_CACHED;
169+
process.stdout.write(`${dir}
170+
`);
171+
return 0;
172+
}
173+
if (command === "install" && rest.length === 4) {
174+
const [version, component, sha256, extracted] = rest;
175+
const dir = await install(version, component, sha256, (tmp) => moveInto(extracted, tmp));
176+
await rm3(extracted, { recursive: true, force: true }).catch(() => {
177+
});
178+
process.stdout.write(`${dir}
179+
`);
180+
return 0;
181+
}
182+
process.stderr.write(`${USAGE}
183+
`);
184+
return 2;
185+
async function moveInto(extractedDir, dir) {
186+
for (const name of await readdir2(extractedDir)) {
187+
const from = join3(extractedDir, name);
188+
const to = join3(dir, name);
189+
try {
190+
await rename2(from, to);
191+
} catch (e) {
192+
if (e.code !== "EXDEV") throw e;
193+
await cp(from, to, { recursive: true, verbatimSymlinks: true });
194+
}
195+
}
196+
}
197+
}
198+
process.exitCode = await main(process.argv.slice(2)).catch((e) => {
199+
process.stderr.write(`${e instanceof Error ? e.message : String(e)}
200+
`);
201+
if (e instanceof InvalidCacheKey) return 2;
202+
return e instanceof CacheNotWritable ? 3 : 1;
203+
});

0 commit comments

Comments
 (0)