ose-auditor 1.0.3 → 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.
- package/ose.js +294 -68
- package/package.json +1 -1
package/ose.js
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ose.js -- npm/npx wrapper for the OSE Auditor Python CLI.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
16
17
|
*
|
|
17
18
|
* Usage:
|
|
18
19
|
* npx ose-auditor audit ./my-project
|
|
@@ -24,20 +25,33 @@
|
|
|
24
25
|
|
|
25
26
|
const { spawnSync } = require("child_process");
|
|
26
27
|
const os = require("os");
|
|
28
|
+
const path = require("path");
|
|
29
|
+
const fs = require("fs");
|
|
27
30
|
|
|
28
31
|
const MIN_PYTHON_MAJOR = 3;
|
|
29
32
|
const MIN_PYTHON_MINOR = 9;
|
|
30
33
|
const PYPI_PACKAGE = "ose-auditor";
|
|
34
|
+
const OSE_VENV_DIR = path.join(os.homedir(), ".ose-venv");
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Generic subprocess helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
|
-
* Run a command
|
|
34
|
-
* for internal probing only -- never used for the final audit invocation.
|
|
35
|
-
*
|
|
36
|
-
* @param {string} command
|
|
37
|
-
* @param {string[]} args
|
|
38
|
-
* @returns {{ok: boolean, stdout: string, stderr: string, status: number|null}}
|
|
41
|
+
* Run a command, capturing output (for probing only -- never for audit output).
|
|
39
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
|
+
|
|
40
51
|
function probe(command, args) {
|
|
52
|
+
if (!ALLOWED_COMMANDS.has(command)) {
|
|
53
|
+
return { ok: false, stdout: "", stderr: "disallowed command", status: null };
|
|
54
|
+
}
|
|
41
55
|
const result = spawnSync(command, args, {
|
|
42
56
|
encoding: "utf8",
|
|
43
57
|
windowsHide: true,
|
|
@@ -54,27 +68,43 @@ function probe(command, args) {
|
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @returns {string|null} The interpreter command name, or null if none found.
|
|
71
|
+
* Run a command with inherited stdio (for install output the user should see).
|
|
72
|
+
* Returns the exit status.
|
|
61
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
|
+
|
|
62
92
|
function findPython() {
|
|
63
|
-
const candidates =
|
|
93
|
+
const candidates =
|
|
94
|
+
os.platform() === "win32"
|
|
95
|
+
? ["py", "python", "python3"]
|
|
96
|
+
: ["python3", "python"];
|
|
64
97
|
|
|
65
98
|
for (const candidate of candidates) {
|
|
66
|
-
const
|
|
99
|
+
const check = probe(candidate, [
|
|
67
100
|
"-c",
|
|
68
101
|
"import sys; print('%d.%d' % sys.version_info[:2])",
|
|
69
102
|
]);
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
const [majorStr, minorStr] = versionCheck.stdout.split(".");
|
|
73
|
-
const major = parseInt(majorStr, 10);
|
|
74
|
-
const minor = parseInt(minorStr, 10);
|
|
103
|
+
if (!check.ok) continue;
|
|
104
|
+
const [maj, min] = check.stdout.split(".").map(Number);
|
|
75
105
|
if (
|
|
76
|
-
|
|
77
|
-
(
|
|
106
|
+
maj > MIN_PYTHON_MAJOR ||
|
|
107
|
+
(maj === MIN_PYTHON_MAJOR && min >= MIN_PYTHON_MINOR)
|
|
78
108
|
) {
|
|
79
109
|
return candidate;
|
|
80
110
|
}
|
|
@@ -82,89 +112,285 @@ function findPython() {
|
|
|
82
112
|
return null;
|
|
83
113
|
}
|
|
84
114
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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) {
|
|
93
172
|
return probe(python, ["-c", "import client.ose"]).ok;
|
|
94
173
|
}
|
|
95
174
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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) {
|
|
103
184
|
process.stderr.write(
|
|
104
|
-
`[ose-auditor]
|
|
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`
|
|
105
206
|
);
|
|
106
207
|
const result = spawnSync(
|
|
107
208
|
python,
|
|
108
209
|
["-m", "pip", "install", "--user", "--upgrade", PYPI_PACKAGE],
|
|
109
|
-
{
|
|
210
|
+
{ encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }
|
|
110
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
|
+
|
|
111
232
|
if (result.status !== 0) {
|
|
112
233
|
process.stderr.write(
|
|
113
|
-
`[ose-auditor] pip install failed (exit
|
|
114
|
-
|
|
234
|
+
`[ose-auditor] pip install failed (exit ${result.status}).\n` +
|
|
235
|
+
`Try manually: ${python} -m pip install --user ${PYPI_PACKAGE}\n`
|
|
115
236
|
);
|
|
116
237
|
return false;
|
|
117
238
|
}
|
|
118
239
|
return true;
|
|
119
240
|
}
|
|
120
241
|
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Main
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
121
246
|
function main() {
|
|
122
247
|
const args = process.argv.slice(2);
|
|
123
248
|
|
|
124
|
-
|
|
125
|
-
|
|
249
|
+
// ------------------------------------------------------------------
|
|
250
|
+
// 1. Find a usable base Python interpreter
|
|
251
|
+
// ------------------------------------------------------------------
|
|
252
|
+
const basePython = findPython();
|
|
253
|
+
if (!basePython) {
|
|
126
254
|
process.stderr.write(
|
|
127
|
-
`[ose-auditor] Could not find a Python ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}+ interpreter
|
|
128
|
-
|
|
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"
|
|
129
257
|
);
|
|
130
258
|
process.exit(1);
|
|
131
259
|
}
|
|
132
260
|
|
|
133
|
-
if (
|
|
134
|
-
|
|
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)) {
|
|
135
265
|
process.stderr.write(
|
|
136
|
-
`[ose-auditor] '${PYPI_PACKAGE}' is not installed and OSE_NO_AUTO_INSTALL=1
|
|
137
|
-
|
|
266
|
+
`[ose-auditor] '${PYPI_PACKAGE}' is not installed and OSE_NO_AUTO_INSTALL=1.\n` +
|
|
267
|
+
`Install manually: pipx install ${PYPI_PACKAGE}\n`
|
|
138
268
|
);
|
|
139
269
|
process.exit(1);
|
|
140
270
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
}
|
|
147
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);
|
|
148
344
|
}
|
|
345
|
+
}
|
|
149
346
|
|
|
347
|
+
/** Launch `ose` (the pipx-injected console script) directly. */
|
|
348
|
+
function launchOseCli(args) {
|
|
150
349
|
if (!process.env.OSE_API_KEY) {
|
|
151
350
|
process.stderr.write(
|
|
152
|
-
"[ose-auditor] Note: OSE_API_KEY is not set.
|
|
153
|
-
|
|
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`
|
|
154
363
|
);
|
|
364
|
+
process.exit(1);
|
|
155
365
|
}
|
|
366
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
367
|
+
}
|
|
156
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
|
+
}
|
|
157
383
|
const result = spawnSync(python, ["-m", "client.ose", ...args], {
|
|
158
384
|
stdio: "inherit",
|
|
159
385
|
env: process.env,
|
|
160
386
|
windowsHide: true,
|
|
161
387
|
});
|
|
162
|
-
|
|
163
388
|
if (result.error) {
|
|
164
|
-
process.stderr.write(
|
|
389
|
+
process.stderr.write(
|
|
390
|
+
`[ose-auditor] Failed to launch Python CLI: ${result.error}\n`
|
|
391
|
+
);
|
|
165
392
|
process.exit(1);
|
|
166
393
|
}
|
|
167
|
-
|
|
168
394
|
process.exit(result.status === null ? 1 : result.status);
|
|
169
395
|
}
|
|
170
396
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ose-auditor",
|
|
3
|
-
"version": "1.0.
|
|
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": {
|