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.
- package/ose.js +397 -0
- 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.
|
|
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": "
|
|
8
|
-
"ose": "
|
|
7
|
+
"ose-audit": "ose.js",
|
|
8
|
+
"ose": "ose.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
11
|
+
"ose.js"
|
|
12
12
|
],
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=16"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "node
|
|
17
|
+
"test": "node ose.js --version"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"security",
|