ose-auditor 1.0.1 → 1.0.4

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 (2) hide show
  1. package/ose.js +397 -0
  2. package/package.json +5 -5
package/ose.js ADDED
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ose.js -- npm/npx wrapper for the OSE Auditor Python CLI.
4
+ *
5
+ * Installation strategy (in priority order):
6
+ * 1. pipx install ose-auditor -- preferred; isolated env, no PEP 668 issues.
7
+ * 2. venv in ~/.ose-venv -- fallback when pipx is absent.
8
+ * 3. pip install --user -- last resort; catches PEP 668 error and
9
+ * advises the user to install pipx instead.
10
+ *
11
+ * After installation the CLI is invoked as:
12
+ * - pipx path: `python -m client.ose` inside the pipx venv, OR
13
+ * - venv path: `<~/.ose-venv>/bin/python -m client.ose`
14
+ *
15
+ * Environment variables forwarded transparently:
16
+ * OSE_API_KEY, OSE_SERVER_URL, OSE_NO_AUTO_INSTALL
17
+ *
18
+ * Usage:
19
+ * npx ose-auditor audit ./my-project
20
+ * npx ose-auditor audit ./my-project --output report.json
21
+ * OSE_API_KEY=sk-... npx ose-auditor audit .
22
+ */
23
+
24
+ "use strict";
25
+
26
+ const { spawnSync } = require("child_process");
27
+ const os = require("os");
28
+ const path = require("path");
29
+ const fs = require("fs");
30
+
31
+ const MIN_PYTHON_MAJOR = 3;
32
+ const MIN_PYTHON_MINOR = 9;
33
+ const PYPI_PACKAGE = "ose-auditor";
34
+ const OSE_VENV_DIR = path.join(os.homedir(), ".ose-venv");
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Generic subprocess helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Run a command, capturing output (for probing only -- never for audit output).
42
+ */
43
+ // Allowlisted commands that may be used as the executable.
44
+ // argv from the user is ONLY ever passed as arguments to these
45
+ // fixed executables -- never as the command itself.
46
+ const ALLOWED_COMMANDS = new Set([
47
+ "pipx", "python3", "python", "py", "pip", "pip3",
48
+ "ose", "ose-mcp",
49
+ ]);
50
+
51
+ function probe(command, args) {
52
+ if (!ALLOWED_COMMANDS.has(command)) {
53
+ return { ok: false, stdout: "", stderr: "disallowed command", status: null };
54
+ }
55
+ const result = spawnSync(command, args, {
56
+ encoding: "utf8",
57
+ windowsHide: true,
58
+ });
59
+ if (result.error) {
60
+ return { ok: false, stdout: "", stderr: String(result.error), status: null };
61
+ }
62
+ return {
63
+ ok: result.status === 0,
64
+ stdout: (result.stdout || "").trim(),
65
+ stderr: (result.stderr || "").trim(),
66
+ status: result.status,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Run a command with inherited stdio (for install output the user should see).
72
+ * Returns the exit status.
73
+ */
74
+ function run(command, args, env) {
75
+ if (!ALLOWED_COMMANDS.has(command)) {
76
+ process.stderr.write(`[ose-auditor] Refused to run disallowed command: ${command}\n`);
77
+ return null;
78
+ }
79
+ const result = spawnSync(command, args, {
80
+ stdio: "inherit",
81
+ windowsHide: true,
82
+ env: env || process.env,
83
+ });
84
+ if (result.error) return null;
85
+ return result.status;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Python interpreter discovery
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function findPython() {
93
+ const candidates =
94
+ os.platform() === "win32"
95
+ ? ["py", "python", "python3"]
96
+ : ["python3", "python"];
97
+
98
+ for (const candidate of candidates) {
99
+ const check = probe(candidate, [
100
+ "-c",
101
+ "import sys; print('%d.%d' % sys.version_info[:2])",
102
+ ]);
103
+ if (!check.ok) continue;
104
+ const [maj, min] = check.stdout.split(".").map(Number);
105
+ if (
106
+ maj > MIN_PYTHON_MAJOR ||
107
+ (maj === MIN_PYTHON_MAJOR && min >= MIN_PYTHON_MINOR)
108
+ ) {
109
+ return candidate;
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // pipx strategy
117
+ // ---------------------------------------------------------------------------
118
+
119
+ function hasPipx() {
120
+ return probe("pipx", ["--version"]).ok;
121
+ }
122
+
123
+ /** Returns the python executable inside the pipx-managed ose-auditor venv. */
124
+ function pipxPython() {
125
+ const result = probe("pipx", ["runpip", PYPI_PACKAGE, "show", "--files"]);
126
+ // Simpler: ask pipx for the venv location via environment inspection.
127
+ // pipx stores venvs in ~/.local/pipx/venvs/<package>/
128
+ const pipxHome =
129
+ process.env.PIPX_HOME ||
130
+ path.join(
131
+ process.env.HOME || os.homedir(),
132
+ os.platform() === "win32" ? "AppData\\Local\\pipx\\pipx" : ".local/pipx"
133
+ );
134
+ const venvBin = path.join(
135
+ pipxHome,
136
+ "venvs",
137
+ PYPI_PACKAGE,
138
+ os.platform() === "win32" ? "Scripts" : "bin",
139
+ os.platform() === "win32" ? "python.exe" : "python"
140
+ );
141
+ if (fs.existsSync(venvBin)) return venvBin;
142
+ // Fallback: pipx inject puts it on PATH as `ose`; use that python
143
+ return null;
144
+ }
145
+
146
+ function isOseInstalledViaPipx() {
147
+ // Check if `ose` CLI is available after pipx install
148
+ return probe("pipx", ["list", "--short"]).stdout.includes(PYPI_PACKAGE);
149
+ }
150
+
151
+ function installViaPipx() {
152
+ process.stderr.write(
153
+ `[ose-auditor] Installing '${PYPI_PACKAGE}' via pipx...\n`
154
+ );
155
+ const status = run("pipx", ["install", "--force", PYPI_PACKAGE]);
156
+ return status === 0;
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // venv (~/.ose-venv) strategy
161
+ // ---------------------------------------------------------------------------
162
+
163
+ function venvPython() {
164
+ const bin =
165
+ os.platform() === "win32"
166
+ ? path.join(OSE_VENV_DIR, "Scripts", "python.exe")
167
+ : path.join(OSE_VENV_DIR, "bin", "python");
168
+ return fs.existsSync(bin) ? bin : null;
169
+ }
170
+
171
+ function isOseInstalledInVenv(python) {
172
+ return probe(python, ["-c", "import client.ose"]).ok;
173
+ }
174
+
175
+ function createVenv(basePython) {
176
+ process.stderr.write(
177
+ `[ose-auditor] Creating virtual environment at ${OSE_VENV_DIR}...\n`
178
+ );
179
+ const status = run(basePython, ["-m", "venv", OSE_VENV_DIR]);
180
+ return status === 0;
181
+ }
182
+
183
+ function installInVenv(python) {
184
+ process.stderr.write(
185
+ `[ose-auditor] Installing '${PYPI_PACKAGE}' into ${OSE_VENV_DIR}...\n`
186
+ );
187
+ const pip =
188
+ os.platform() === "win32"
189
+ ? path.join(OSE_VENV_DIR, "Scripts", "pip.exe")
190
+ : path.join(OSE_VENV_DIR, "bin", "pip");
191
+ const status = run(pip, ["install", "--upgrade", PYPI_PACKAGE]);
192
+ return status === 0;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // pip --user fallback (last resort)
197
+ // ---------------------------------------------------------------------------
198
+
199
+ function isOseInstalledGlobally(python) {
200
+ return probe(python, ["-c", "import client.ose"]).ok;
201
+ }
202
+
203
+ function installViaPipUser(python) {
204
+ process.stderr.write(
205
+ `[ose-auditor] Attempting pip install --user '${PYPI_PACKAGE}'...\n`
206
+ );
207
+ const result = spawnSync(
208
+ python,
209
+ ["-m", "pip", "install", "--user", "--upgrade", PYPI_PACKAGE],
210
+ { encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }
211
+ );
212
+
213
+ const combinedOutput = (result.stdout || "") + (result.stderr || "");
214
+ if (
215
+ combinedOutput.includes("externally-managed-environment") ||
216
+ combinedOutput.includes("PEP 668")
217
+ ) {
218
+ process.stderr.write(
219
+ "\n[ose-auditor] ✗ pip install was blocked by your system Python (PEP 668 / externally-managed-environment).\n" +
220
+ " Fix: install pipx, then re-run npx ose-auditor:\n\n" +
221
+ " # macOS\n" +
222
+ " brew install pipx && pipx ensurepath\n\n" +
223
+ " # Linux\n" +
224
+ " sudo apt install pipx && pipx ensurepath\n\n" +
225
+ " # Windows (PowerShell)\n" +
226
+ " python -m pip install --user pipx\n\n" +
227
+ " Then re-run: npx ose-auditor audit .\n\n"
228
+ );
229
+ return false;
230
+ }
231
+
232
+ if (result.status !== 0) {
233
+ process.stderr.write(
234
+ `[ose-auditor] pip install failed (exit ${result.status}).\n` +
235
+ `Try manually: ${python} -m pip install --user ${PYPI_PACKAGE}\n`
236
+ );
237
+ return false;
238
+ }
239
+ return true;
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Main
244
+ // ---------------------------------------------------------------------------
245
+
246
+ function main() {
247
+ const args = process.argv.slice(2);
248
+
249
+ // ------------------------------------------------------------------
250
+ // 1. Find a usable base Python interpreter
251
+ // ------------------------------------------------------------------
252
+ const basePython = findPython();
253
+ if (!basePython) {
254
+ process.stderr.write(
255
+ `[ose-auditor] Could not find a Python ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}+ interpreter.\n` +
256
+ "Install Python from https://python.org and ensure 'python3' is on your PATH.\n"
257
+ );
258
+ process.exit(1);
259
+ }
260
+
261
+ if (process.env.OSE_NO_AUTO_INSTALL === "1") {
262
+ // Skip install entirely; trust that the package is already available.
263
+ const p = venvPython() || basePython;
264
+ if (!isOseInstalledInVenv(p) && !isOseInstalledGlobally(basePython)) {
265
+ process.stderr.write(
266
+ `[ose-auditor] '${PYPI_PACKAGE}' is not installed and OSE_NO_AUTO_INSTALL=1.\n` +
267
+ `Install manually: pipx install ${PYPI_PACKAGE}\n`
268
+ );
269
+ process.exit(1);
270
+ }
271
+ launchCli(p, args);
272
+ return;
273
+ }
274
+
275
+ // ------------------------------------------------------------------
276
+ // 2. Strategy A: pipx
277
+ // ------------------------------------------------------------------
278
+ if (hasPipx()) {
279
+ if (!isOseInstalledViaPipx()) {
280
+ const ok = installViaPipx();
281
+ if (!ok) {
282
+ process.stderr.write("[ose-auditor] pipx install failed.\n");
283
+ // fall through to venv strategy
284
+ }
285
+ }
286
+ if (isOseInstalledViaPipx()) {
287
+ // Find the python inside pipx's managed venv
288
+ const pp = pipxPython();
289
+ if (pp) {
290
+ launchCli(pp, args);
291
+ return;
292
+ }
293
+ // pipx puts `ose` on PATH directly; try invoking it that way
294
+ launchOseCli(args);
295
+ return;
296
+ }
297
+ }
298
+
299
+ // ------------------------------------------------------------------
300
+ // 3. Strategy B: ~/.ose-venv
301
+ // ------------------------------------------------------------------
302
+ {
303
+ let vPython = venvPython();
304
+ if (!vPython) {
305
+ const created = createVenv(basePython);
306
+ if (!created) {
307
+ process.stderr.write(
308
+ `[ose-auditor] Failed to create venv at ${OSE_VENV_DIR}.\n`
309
+ );
310
+ // fall through to pip --user
311
+ } else {
312
+ vPython = venvPython();
313
+ }
314
+ }
315
+
316
+ if (vPython) {
317
+ if (!isOseInstalledInVenv(vPython)) {
318
+ const ok = installInVenv(vPython);
319
+ if (!ok) {
320
+ process.stderr.write("[ose-auditor] venv install failed.\n");
321
+ // fall through
322
+ }
323
+ }
324
+ if (isOseInstalledInVenv(vPython)) {
325
+ launchCli(vPython, args);
326
+ return;
327
+ }
328
+ }
329
+ }
330
+
331
+ // ------------------------------------------------------------------
332
+ // 4. Strategy C: pip install --user (last resort)
333
+ // ------------------------------------------------------------------
334
+ {
335
+ const ok = installViaPipUser(basePython);
336
+ if (ok && isOseInstalledGlobally(basePython)) {
337
+ launchCli(basePython, args);
338
+ return;
339
+ }
340
+ process.stderr.write(
341
+ "[ose-auditor] All installation strategies failed. Cannot continue.\n"
342
+ );
343
+ process.exit(1);
344
+ }
345
+ }
346
+
347
+ /** Launch `ose` (the pipx-injected console script) directly. */
348
+ function launchOseCli(args) {
349
+ if (!process.env.OSE_API_KEY) {
350
+ process.stderr.write(
351
+ "[ose-auditor] Note: OSE_API_KEY is not set. " +
352
+ "Audits with findings will need it to fetch patches (https://ose.crestsek.com).\n"
353
+ );
354
+ }
355
+ const result = spawnSync("ose", args, {
356
+ stdio: "inherit",
357
+ env: process.env,
358
+ windowsHide: true,
359
+ });
360
+ if (result.error) {
361
+ process.stderr.write(
362
+ `[ose-auditor] Failed to launch 'ose': ${result.error}\n`
363
+ );
364
+ process.exit(1);
365
+ }
366
+ process.exit(result.status === null ? 1 : result.status);
367
+ }
368
+
369
+ /** Launch `python -m client.ose` with the given interpreter. */
370
+ function launchCli(python, args) {
371
+ // python is always a value returned by findPython(), which only
372
+ // returns strings from the fixed ALLOWED_COMMANDS set above.
373
+ if (!ALLOWED_COMMANDS.has(python) && !require("fs").existsSync(python)) {
374
+ process.stderr.write(`[ose-auditor] Refused to execute unlisted interpreter: ${python}\n`);
375
+ process.exit(1);
376
+ }
377
+ if (!process.env.OSE_API_KEY) {
378
+ process.stderr.write(
379
+ "[ose-auditor] Note: OSE_API_KEY is not set. " +
380
+ "Audits with findings will need it to fetch patches (https://ose.crestsek.com).\n"
381
+ );
382
+ }
383
+ const result = spawnSync(python, ["-m", "client.ose", ...args], {
384
+ stdio: "inherit",
385
+ env: process.env,
386
+ windowsHide: true,
387
+ });
388
+ if (result.error) {
389
+ process.stderr.write(
390
+ `[ose-auditor] Failed to launch Python CLI: ${result.error}\n`
391
+ );
392
+ process.exit(1);
393
+ }
394
+ process.exit(result.status === null ? 1 : result.status);
395
+ }
396
+
397
+ main();
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "ose-auditor",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Run OSE Auditor (financial-logic security scanner for Node.js/TypeScript) without installing Python yourself -- this wrapper finds a Python 3 interpreter, installs the ose-auditor PyPI package on first run if needed, and forwards all arguments to it.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
- "ose-audit": "bin/ose.js",
8
- "ose": "bin/ose.js"
7
+ "ose-audit": "ose.js",
8
+ "ose": "ose.js"
9
9
  },
10
10
  "files": [
11
- "bin/"
11
+ "ose.js"
12
12
  ],
13
13
  "engines": {
14
14
  "node": ">=16"
15
15
  },
16
16
  "scripts": {
17
- "test": "node bin/ose.js --version"
17
+ "test": "node ose.js --version"
18
18
  },
19
19
  "keywords": [
20
20
  "security",